自定义 ViewGroup 全屏选中作用
前语
工作是这个姿态的,前几天产品丢给我一个视频,你觉得这个作用怎样样?咱们的 App 也做一个这个作用吧!
我其时的反应:
开什么打趣!就没见过这么玩的,这不是坑人吗?
此时产品幽幽的回了一句,“别人都能做,你怎样不能做,并且iOS说能够做,还很简略。”
我心里一万个不信,糟老头子太坏了,想骗我?
我立马和iOS同事统一战线,说不能做,结束不了吧。成果iOS同事幽幽的说了一句 “现已做了,四行代码结束”。
我勒个去,就指着我卷是吧。
这也没办法了,群里问问大神有什么好的计划,“xdm,车先减个速,(图片)这个作用怎样结束?”
“做不了…”
“让产品滚…”
“没做过,也没见过…”
“性能不好,不引荐,换计划吧。”
“GridView嵌套ScrollView , 要不RV嵌套RV?…”
“不理他,持续开车…”
…群里技能气氛果然没有让我绝望,哎,看来仍是得靠自己,昂首望了望天天,扣了扣脑阔,无语啊。
好了,说了这么多打趣话,回归正题,其实关于标题的这种作用,确实是对性能的开销更大,且网上相关开源的项目也几乎没找到。
究竟怎样做呢?相信跟着我一同温习的小伙伴们心里都有了一点雏形。自定义ViewGroup。
下面跟着我一同再次稳固一次 ViewGroup 的丈量与布局,加上事情的处理,就能结束对应的功用。
话不多说,Let’s go
一、布局的丈量与布局
首要GridView嵌套ScrollView,RV 嵌套 RV 什么的,就宽度就约束死了,其次滚动方向也固定死了,不好做。
肯定是选用自定义 ViewGroup 的计划,自己丈量,自己布局,自己结束滚动与缩放逻辑。
从产品发的竞品App的视频来看,咱们需求先明确三个变量,一行显示多少个Item、笔直间隔每一个Item的距离,水平间隔每一个Item的距离。
然后咱们丈量每一个ItemView的宽度,每一个Item的宽度加起来便是ViewGroup的宽度,每一个Item的高度加起来便是ViewGroup的高度。
咱们目前先不限定Item的宽高,先试着丈量一下:
class CurtainViewContrainer extends ViewGroup {
private int horizontalSpacing = 20; //每一个Item的左右距离
private int verticalSpacing = 20; //每一个Item的上下距离
private int mRowCount = 6; // 一行多少个Item
private Adapter mAdapter;
public CurtainViewContrainer(Context context) {
this(context, null);
}
public CurtainViewContrainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CurtainViewContrainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setClipChildren(false);
setClipToPadding(false);
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft();
final int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom();
final int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
if (mAdapter == null || mAdapter.getItemCount() == 0 || childCount == 0) {
setMeasuredDimension(sizeWidth, 0);
return;
}
int curCount = 1;
int totalControlHeight = 0;
int totalControlWidth = 0;
int layoutChildViewCurX = this.getPaddingLeft();
int curRow = 0;
int curColumn = 0;
SparseArray<Integer> rowWidth = new SparseArray<>(); //悉数行的宽度
//开端遍历
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int row = curCount / mRowCount; //当时子View是第几行
int column = curCount % mRowCount; //当时子View是第几列
//丈量每一个子View宽度
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
boolean isLast = (curCount + 1) % mRowCount == 0;
if (row == curRow) {
layoutChildViewCurX += width + horizontalSpacing;
totalControlWidth += width + horizontalSpacing;
rowWidth.put(row, totalControlWidth);
} else {
//现已换行了
layoutChildViewCurX = this.getPaddingLeft();
totalControlWidth = width + horizontalSpacing;
rowWidth.put(row, totalControlWidth);
//增加高度
totalControlHeight += height + verticalSpacing;
}
//最多只摆放9个
curCount++;
curRow = row;
curColumn = column;
}
//循环完毕之后开端核算真实的宽度
List<Integer> widthList = new ArrayList<>(rowWidth.size());
for (int i = 0; i < rowWidth.size(); i++) {
Integer integer = rowWidth.get(i);
widthList.add(integer);
}
Integer maxWidth = Collections.max(widthList);
setMeasuredDimension(maxWidth, totalControlHeight);
}
当遇到高度不统一的情况下,就会遇到问题,所以咱们记载一下每一行的最高高度,用于核算控件的丈量高度。
尽管这样丈量是没有问题的,可是布局仍是有坑,权且先这么丈量:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int curCount = 1;
int layoutChildViewCurX = l;
int layoutChildViewCurY = t;
int curRow = 0;
int curColumn = 0;
SparseArray<Integer> rowWidth = new SparseArray<>(); //悉数行的宽度
//开端遍历
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int row = curCount / mRowCount; //当时子View是第几行
int column = curCount % mRowCount; //当时子View是第几列
//每一个子View宽度
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height);
if (row == curRow) {
//同一行
layoutChildViewCurX += width + horizontalSpacing;
} else {
//换行了
layoutChildViewCurX = l;
layoutChildViewCurY += height + verticalSpacing;
}
//最多只摆放9个
curCount++;
curRow = row;
curColumn = column;
}
performBindData();
}
这样做并没有紧挨着头上的Item,目前咱们把Item的宽高都运用同样的大小,是牵强能看的,一旦高度不统一,就不能看了。
先不管那么多,先固定大小显示出来看看作用。
反正是能看了,一个寨版的 GridView ,可是超出了宽度的约束。接下来咱们先做事情的处理,让他动起来。
二、全屏滚动逻辑
首要咱们需求把显示的 ViewGroup 控件封装为一个类,让此ViewGroup在另一个ViewGroup内部移动,否则还能让内部的每一个子View单独移动吗?肯定是全体一同移动更方便一点。
然后咱们接触容器 ViewGroup 中操控子 ViewGroup 移动即可,那怎样移动呢?
我知道,用 MotionEvent + Scroller 就能够滚动啦!
能够!又不能够,Scroller确实是能够动起来,可是在咱们拖动与缩放之后,不能影响到内部的点击事情。
那能够不能够用 ViewDragHelper 来结束动作作用?
也不可,尽管 ViewDragHelper 是ViewGroup专门用于移动的协助类,可是它内部其实仍是封装的 MotionEvent + Scroller。
而 Scroller 为什么不可?
这种作用咱们不能运用 Canvas 的移动,不能运用 Sroller 去移动,因为它们不能记载移动后的 View 改变矩阵,咱们需求运用基本的 setTranslation 来结束,自己操控矩阵的改变然后操控整个视图树。
咱们把接触的阻拦与事情的处理放到一个共用的事情处理类中:
public class TouchEventHandler {
private static final float MAX_SCALE = 1.5f; //最大能缩放值
private static final float MIN_SCALE = 0.8f; //最小能缩放值
//当时的接触事情类型
private static final int TOUCH_MODE_UNSET = -1;
private static final int TOUCH_MODE_RELEASE = 0;
private static final int TOUCH_MODE_SINGLE = 1;
private static final int TOUCH_MODE_DOUBLE = 2;
private View mView;
private int mode = 0;
private float scaleFactor = 1.0f;
private float scaleBaseR;
private GestureDetector mGestureDetector;
private float mTouchSlop;
private MotionEvent preMovingTouchEvent = null;
private MotionEvent preInterceptTouchEvent = null;
private boolean mIsMoving;
private float minScale = MIN_SCALE;
private FlingAnimation flingY = null;
private FlingAnimation flingX = null;
private ViewBox layoutLocationInParent = new ViewBox(); //移动中不断改变的盒模型
private final ViewBox viewportBox = new ViewBox(); //初始化的盒模型
private PointF preFocusCenter = new PointF();
private PointF postFocusCenter = new PointF();
private PointF preTranslate = new PointF();
private float preScaleFactor = 1f;
private final DynamicAnimation.OnAnimationUpdateListener flingAnimateListener;
private boolean isKeepInViewport = false;
private TouchEventListener controlListener = null;
private int scalePercentOnlyForControlListener = 0;
public TouchEventHandler(Context context, View view) {
this.mView = view;
flingAnimateListener = (animation, value, velocity) -> keepWithinBoundaries();
mGestureDetector = new GestureDetector(context,
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
flingX = new FlingAnimation(mView, DynamicAnimation.TRANSLATION_X);
flingX.setStartVelocity(velocityX)
.addUpdateListener(flingAnimateListener)
.start();
flingY = new FlingAnimation(mView, DynamicAnimation.TRANSLATION_Y);
flingY.setStartVelocity(velocityY)
.addUpdateListener(flingAnimateListener)
.start();
return false;
}
});
ViewConfiguration vc = ViewConfiguration.get(view.getContext());
mTouchSlop = vc.getScaledTouchSlop() * 0.8f;
}
/**
* 设置内部布局视图窗口高度和宽度
*/
public void setViewport(int winWidth, int winHeight) {
viewportBox.setValues(0, 0, winWidth, winHeight);
}
/**
* 暴露的办法,内部处理事情并判别是否阻拦事情
*/
public boolean detectInterceptTouchEvent(MotionEvent event) {
final int action = event.getAction() & MotionEvent.ACTION_MASK;
onTouchEvent(event);
if (action == MotionEvent.ACTION_DOWN) {
preInterceptTouchEvent = MotionEvent.obtain(event);
mIsMoving = false;
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsMoving = false;
}
if (action == MotionEvent.ACTION_MOVE && mTouchSlop < calculateMoveDistance(event, preInterceptTouchEvent)) {
mIsMoving = true;
}
return mIsMoving;
}
/**
* 当时事情的真实处理逻辑
*/
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_DOWN:
mode = TOUCH_MODE_SINGLE;
preMovingTouchEvent = MotionEvent.obtain(event);
if (flingX != null) {
flingX.cancel();
}
if (flingY != null) {
flingY.cancel();
}
break;
case MotionEvent.ACTION_UP:
mode = TOUCH_MODE_RELEASE;
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
mode = TOUCH_MODE_UNSET;
break;
case MotionEvent.ACTION_POINTER_DOWN:
mode++;
if (mode >= TOUCH_MODE_DOUBLE) {
scaleFactor = preScaleFactor = mView.getScaleX();
preTranslate.set(mView.getTranslationX(), mView.getTranslationY());
scaleBaseR = (float) distanceBetweenFingers(event);
centerPointBetweenFingers(event, preFocusCenter);
centerPointBetweenFingers(event, postFocusCenter);
}
break;
case MotionEvent.ACTION_MOVE:
if (mode >= TOUCH_MODE_DOUBLE) {
//双指缩放
float scaleNewR = (float) distanceBetweenFingers(event);
centerPointBetweenFingers(event, postFocusCenter);
if (scaleBaseR <= 0) {
break;
}
scaleFactor = (scaleNewR / scaleBaseR) * preScaleFactor * 0.15f + scaleFactor * 0.85f;
int scaleState = TouchEventListener.FREE_SCALE;
float finalMinScale = isKeepInViewport ? minScale : minScale * 0.8f;
if (scaleFactor >= MAX_SCALE) {
scaleFactor = MAX_SCALE;
scaleState = TouchEventListener.MAX_SCALE;
} else if (scaleFactor <= finalMinScale) {
scaleFactor = finalMinScale;
scaleState = TouchEventListener.MIN_SCALE;
}
if (controlListener != null) {
int current = (int) (scaleFactor * 100);
//回调
if (scalePercentOnlyForControlListener != current) {
scalePercentOnlyForControlListener = current;
controlListener.onScaling(scaleState, scalePercentOnlyForControlListener);
}
}
mView.setPivotX(0);
mView.setPivotY(0);
mView.setScaleX(scaleFactor);
mView.setScaleY(scaleFactor);
float tx = postFocusCenter.x - (preFocusCenter.x - preTranslate.x) * scaleFactor / preScaleFactor;
float ty = postFocusCenter.y - (preFocusCenter.y - preTranslate.y) * scaleFactor / preScaleFactor;
mView.setTranslationX(tx);
mView.setTranslationY(ty);
keepWithinBoundaries();
} else if (mode == TOUCH_MODE_SINGLE) {
//单指移动
float deltaX = event.getRawX() - preMovingTouchEvent.getRawX();
float deltaY = event.getRawY() - preMovingTouchEvent.getRawY();
onSinglePointMoving(deltaX, deltaY);
}
break;
case MotionEvent.ACTION_OUTSIDE:
//外界的事情
break;
}
preMovingTouchEvent = MotionEvent.obtain(event);
return true;
}
/**
* 核算两个事情的移动间隔
*/
private float calculateMoveDistance(MotionEvent event1, MotionEvent event2) {
if (event1 == null || event2 == null) {
return 0f;
}
float disX = Math.abs(event1.getRawX() - event2.getRawX());
float disY = Math.abs(event1.getRawX() - event2.getRawX());
return (float) Math.sqrt(disX * disX + disY * disY);
}
/**
* 单指移动
*/
private void onSinglePointMoving(float deltaX, float deltaY) {
float translationX = mView.getTranslationX() + deltaX;
mView.setTranslationX(translationX);
float translationY = mView.getTranslationY() + deltaY;
mView.setTranslationY(translationY);
keepWithinBoundaries();
}
/**
* 需求坚持在界限之内
*/
private void keepWithinBoundaries() {
//默认不在界限内,不做约束,直接回来
if (!isKeepInViewport) {
return;
}
calculateBound();
int dBottom = layoutLocationInParent.bottom - viewportBox.bottom;
int dTop = layoutLocationInParent.top - viewportBox.top;
int dLeft = layoutLocationInParent.left - viewportBox.left;
int dRight = layoutLocationInParent.right - viewportBox.right;
float translationX = mView.getTranslationX();
float translationY = mView.getTranslationY();
//边界约束
if (dLeft > 0) {
mView.setTranslationX(translationX - dLeft);
}
if (dRight < 0) {
mView.setTranslationX(translationX - dRight);
}
if (dBottom < 0) {
mView.setTranslationY(translationY - dBottom);
}
if (dTop > 0) {
mView.setTranslationY(translationY - dTop);
}
}
/**
* 移动时核算边界,赋值给本地的视图
*/
private void calculateBound() {
View v = mView;
float left = v.getLeft() * v.getScaleX() + v.getTranslationX();
float top = v.getTop() * v.getScaleY() + v.getTranslationY();
float right = v.getRight() * v.getScaleX() + v.getTranslationX();
float bottom = v.getBottom() * v.getScaleY() + v.getTranslationY();
layoutLocationInParent.setValues((int) top, (int) left, (int) right, (int) bottom);
}
/**
* 核算两个手指之间的间隔
*/
private double distanceBetweenFingers(MotionEvent event) {
if (event.getPointerCount() > 1) {
float disX = Math.abs(event.getX(0) - event.getX(1));
float disY = Math.abs(event.getY(0) - event.getY(1));
return Math.sqrt(disX * disX + disY * disY);
}
return 1;
}
/**
* 核算两个手指之间的中心点
*/
private void centerPointBetweenFingers(MotionEvent event, PointF point) {
float xPoint0 = event.getX(0);
float yPoint0 = event.getY(0);
float xPoint1 = event.getX(1);
float yPoint1 = event.getY(1);
point.set((xPoint0 + xPoint1) / 2f, (yPoint0 + yPoint1) / 2f);
}
/**
* 设置视图是否要坚持在窗口中
*/
public void setKeepInViewport(boolean keepInViewport) {
isKeepInViewport = keepInViewport;
}
/**
* 设置操控的监听回调
*/
public void setControlListener(TouchEventListener controlListener) {
this.controlListener = controlListener;
}
}
因为内部封装了移动与缩放的处理,所以咱们只需求在事情容器内部调用这个办法即可:
public class CurtainLayout extends FrameLayout {
private final TouchEventHandler mGestureHandler;
private CurtainViewContrainer mCurtainViewContrainer;
private boolean disallowIntercept = false;
public CurtainLayout(@NonNull Context context) {
this(context, null);
}
public CurtainLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CurtainLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClipChildren(false);
setClipToPadding(false);
mCurtainViewContrainer = new CurtainViewContrainer(getContext());
addView(mCurtainViewContrainer);
mGestureHandler = new TouchEventHandler(getContext(), mCurtainViewContrainer);
//设置是否在窗口内移动
mGestureHandler.setKeepInViewport(false);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
this.disallowIntercept = disallowIntercept;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return (!disallowIntercept && mGestureHandler.detectInterceptTouchEvent(event)) || super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return !disallowIntercept && mGestureHandler.onTouchEvent(event);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mGestureHandler.setViewport(w, h);
}
}
关于一些杂乱的处理都做了相关的注释,接下来看看加了事情处理之后的作用:
现已能够自在拖动与缩放了,可是目前的丈量与布局是有问题的,加下来咱们抽取与优化一下。
三、抽取Adapter与LayoutManager
首要,内部的子View肯定是不能直接写在 xml 中的,太不优雅了,加下来咱们定义一个Adapter,用于填充数据,趁便做一个多类型的布局。
public abstract class CurtainAdapter {
//回来总共子View的数量
public abstract int getItemCount();
//依据索引创建不同的布局类型,假如都是相同的布局则不需求重写
public int getItemViewType(int position) {
return 0;
}
//依据类型创建对应的View布局
public abstract View onCreateItemView(@NonNull Context context, @NonNull ViewGroup parent, int itemType);
//能够依据类型或索引绑定数据
public abstract void onBindItemView(@NonNull View itemView, int itemType, int position);
}
然后便是在绘制布局中经过设置 Apdater 来结束布局的增加与绑定逻辑。
public void setAdapter(CurtainAdapter adapter) {
mAdapter = adapter;
inflateAllViews();
}
public CurtainAdapter getAdapter() {
return mAdapter;
}
//填充Adapter布局
private void inflateAllViews() {
removeAllViewsInLayout();
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
//增加布局
for (int i = 0; i < mAdapter.getItemCount(); i++) {
int itemType = mAdapter.getItemViewType(i);
View view = mAdapter.onCreateItemView(getContext(), this, itemType);
addView(view);
}
requestLayout();
}
//绑定布局中的数据
private void performBindData() {
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
post(() -> {
for (int i = 0; i < mAdapter.getItemCount(); i++) {
int itemType = mAdapter.getItemViewType(i);
View view = getChildAt(i);
mAdapter.onBindItemView(view, itemType, i);
}
});
}
当然需求在指定的当地调用了,丈量与布局中都需求处理。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
if (mAdapter == null || mAdapter.getItemCount() == 0 || childCount == 0) {
setMeasuredDimension(0, 0);
return;
}
...
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
performLayout();
performBindData();
}
接下来的重点便是咱们对布局的方法进行抽象化,最简略的肯定是上面这种宽高固定的,假如是笔直的摆放,咱们设置一个笔直的瀑布流管理器,设置宽度固定,高度自适应,假如宽度不固定,那么是无法抵达瀑布流的作用的。
同理对另一种水平摆放的瀑布流咱们设置高度固定,宽度自适应。
所以必需求设置 LayoutManager,假如不设置就抛反常。
接下来便是 LayoutManager 的接口与详细调用:
public interface ILayoutManager {
public static final int DIRECTION_VERITICAL = 0;
public static final int DIRECTION_HORIZONTAL = 1;
public abstract int[] performMeasure(ViewGroup viewGroup, int rowCount, int horizontalSpacing, int verticalSpacing, int fixedValue);
public abstract void performLayout(ViewGroup viewGroup, int rowCount, int horizontalSpacing, int verticalSpacing, int fixedValue);
public abstract int getLayoutDirection();
}
有了接口之后咱们就能够先写调用了:
class CurtainViewContrainer extends ViewGroup {
private ILayoutManager mLayoutManager;
private int horizontalSpacing = 20; //每一个Item的左右距离
private int verticalSpacing = 20; //每一个Item的上下距离
private int mRowCount = 6; // 一行多少个Item
private int fixedWidth = CommUtils.dip2px(150); //假如是笔直瀑布流,需求设置宽度固定
private int fixedHeight = CommUtils.dip2px(180); //先写死,后期在抽取特点
private CurtainAdapter mAdapter;
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
if (mAdapter == null || mAdapter.getItemCount() == 0 || childCount == 0) {
setMeasuredDimension(0, 0);
return;
}
measureChildren(widthMeasureSpec, heightMeasureSpec);
if (mLayoutManager != null && (fixedWidth > 0 || fixedHeight > 0)) {
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (mLayoutManager.getLayoutDirection() == ILayoutManager.DIRECTION_VERITICAL) {
measureChild(childView,
MeasureSpec.makeMeasureSpec(fixedWidth, MeasureSpec.EXACTLY),
heightMeasureSpec);
} else {
measureChild(childView,
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(fixedHeight, MeasureSpec.EXACTLY));
}
}
int[] dimensions = mLayoutManager.performMeasure(this, mRowCount, horizontalSpacing, verticalSpacing,
mLayoutManager.getLayoutDirection() == ILayoutManager.DIRECTION_VERITICAL ? fixedWidth : fixedHeight);
setMeasuredDimension(dimensions[0], dimensions[1]);
} else {
throw new RuntimeException("You need to set the layoutManager first");
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mAdapter == null || mAdapter.getItemCount() == 0) {
return;
}
if (mLayoutManager != null && (fixedWidth > 0 || fixedHeight > 0)) {
mLayoutManager.performLayout(this, mRowCount, horizontalSpacing, verticalSpacing,
mLayoutManager.getLayoutDirection() == ILayoutManager.DIRECTION_VERITICAL ? fixedWidth : fixedHeight);
performBindData();
} else {
throw new RuntimeException("You need to set the layoutManager first");
}
}
那么咱们先来水平的LayoutManager,相对简略一些,看看怎么详细结束:
public class HorizontalLayoutManager implements ILayoutManager {
@Override
public int[] performMeasure(ViewGroup viewGroup, int rowCount, int horizontalSpacing, int verticalSpacing, int fixedHeight) {
int childCount = viewGroup.getChildCount();
int curCount = 0;
int totalControlHeight = 0;
int totalControlWidth = 0;
int curRow = 0;
SparseArray<Integer> rowTotalWidth = new SparseArray<>(); //每一行的总宽度
//开端遍历
for (int i = 0; i < childCount; i++) {
View childView = viewGroup.getChildAt(i);
int row = curCount / rowCount; //当时子View是第几行
//现已丈量过了,直接取宽高
int width = childView.getMeasuredWidth();
if (row == curRow) {
//当时行
totalControlWidth += width + horizontalSpacing;
} else {
//换行了
totalControlWidth = width + horizontalSpacing;
}
rowTotalWidth.put(row, totalControlWidth);
//赋值
curCount++;
curRow = row;
}
//循环完毕之后开端核算真实的宽高
totalControlHeight = (rowCount * (fixedHeight + verticalSpacing)) - verticalSpacing +
viewGroup.getPaddingTop() + viewGroup.getPaddingBottom();
List<Integer> widthList = new ArrayList<>();
for (int i = 0; i < rowTotalWidth.size(); i++) {
Integer width = rowTotalWidth.get(i);
widthList.add(width);
}
totalControlWidth = Collections.max(widthList);
rowTotalWidth.clear();
rowTotalWidth = null;
return new int[]{totalControlWidth - horizontalSpacing, totalControlHeight - verticalSpacing};
}
@Override
public void performLayout(ViewGroup viewGroup, int rowCount, int horizontalSpacing, int verticalSpacing, int fixedHeight) {
int childCount = viewGroup.getChildCount();
int curCount = 1;
int layoutChildViewCurX = viewGroup.getPaddingLeft();
int layoutChildViewCurY = viewGroup.getPaddingTop();
int curRow = 0;
//开端遍历
for (int i = 0; i < childCount; i++) {
View childView = viewGroup.getChildAt(i);
int row = curCount / rowCount; //当时子View是第几行
//每一个子View宽度
int width = childView.getMeasuredWidth();
childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + fixedHeight);
if (row == curRow) {
//同一行
layoutChildViewCurX += width + horizontalSpacing;
} else {
//换行了
layoutChildViewCurX = childView.getPaddingLeft();
layoutChildViewCurY += fixedHeight + verticalSpacing;
}
//赋值
curCount++;
curRow = row;
}
}
@Override
public int getLayoutDirection() {
return DIRECTION_HORIZONTAL;
}
}
关于水平的布局方法来说,高度是固定的,咱们很简单的就能核算出来,可是宽度每一行的或许都不相同,咱们用一个List记载每一行的总宽度,在终究设置的时分取出最大的一行作为容器的宽度,记得要减去一个距离哦。
那么不同宽度的水平布局方法作用的结束便是这样:
结束是结束了,可是这么核算是不是有问题?每一行的最高高度如同不是太精确,假如每一列都有一个最大高度,可是不是同一列,那么丈量的高度就比实践高度要更高。
加一个灰色布景就能够看到作用:
咱们再优化一下,它应该是核算每一列的总共高度,然后选出最大高度才对:
@Override
public int[] performMeasure(ViewGroup viewGroup, int rowCount, int horizontalSpacing, int verticalSpacing, int fixedWidth) {
int childCount = viewGroup.getChildCount();
int curPosition = 0;
int totalControlHeight = 0;
int totalControlWidth = 0;
SparseArray<List<Integer>> columnAllHeight = new SparseArray<>(); //每一列的悉数高度
//开端遍历
for (int i = 0; i < childCount; i++) {
View childView = viewGroup.getChildAt(i);
int row = curPosition / rowCount; //当时子View是第几行
int column = curPosition % rowCount; //当时子View是第几列
//现已丈量过了,直接取宽高
int height = childView.getMeasuredHeight();
List<Integer> integers = columnAllHeight.get(column);
if (integers == null || integers.isEmpty()) {
integers = new ArrayList<>();
}
integers.add(height + verticalSpacing);
columnAllHeight.put(column, integers);
//赋值
curPosition++;
}
//循环完毕之后开端核算真实的宽高
totalControlWidth = (rowCount *
(fixedWidth + horizontalSpacing) + viewGroup.getPaddingLeft() + viewGroup.getPaddingRight());
List<Integer> totalHeights = new ArrayList<>();
for (int i = 0; i < columnAllHeight.size(); i++) {
List<Integer> heights = columnAllHeight.get(i);
int totalHeight = 0;
for (int j = 0; j < heights.size(); j++) {
totalHeight += heights.get(j);
}
totalHeights.add(totalHeight);
}
totalControlHeight = Collections.max(totalHeights);
columnAllHeight.clear();
columnAllHeight = null;
return new int[]{totalControlWidth - horizontalSpacing, totalControlHeight - verticalSpacing};
}
再看看作用:
宽高真实的丈量精确之后咱们接下来就开端特点的抽取与封装了。
四、自定义特点
咱们从前都是运用的成员变量来操控一些距离与逻辑的触发,这就跟业务耦合了,假如想做到通用的一个作用,肯定仍是要抽取自定义特点,做到对应的配置开关,就能够适应更多的场景运用,也是开源项目的必备技能。
细数一下咱们需求操控的特点:
- enableScale 是否支撑缩放
- maxScale 缩放的最大份额
- minScale 缩放的最小份额
- moveInViewport 是否只能在布局内部移动
- horizontalSpacing item的水平距离
- verticalSpacing item的笔直距离
- fixed_width 竖向的摆放 – 宽度定死 并设置对应的LayoutManager
- fixed_height 横向的摆放 – 高度定死 并设置对应的LayoutManager
定义特点如下:
<!-- 全屏幕布布局自定义特点 -->
<declare-styleable name="CurtainLayout">
<!--Item的横向距离-->
<attr name="horizontalSpacing" format="dimension" />
<!--Item的笔直距离-->
<attr name="verticalSpacing" format="dimension" />
<!--每行需求展示多少数量的Item-->
<attr name="rowCount" format="integer" />
<!--笔直方向瀑布流布局,固定宽度为多少-->
<attr name="fixedWidth" format="dimension" />
<!--水平方向瀑布流布局,固定高度为多少-->
<attr name="fixedHeight" format="dimension" />
<!--是否只能在布局内部移动 当为false时分为自在移动-->
<attr name="moveInViewport" format="boolean" />
<!--是否能够缩放-->
<attr name="enableScale" format="boolean" />
<!--最大与最小的缩放份额-->
<attr name="maxScale" format="float" />
<attr name="minScale" format="float" />
</declare-styleable>
取出特点并对容器布局与接触处理器做赋值的操作:
public class CurtainLayout extends FrameLayout {
private int horizontalSpacing;
private int verticalSpacing;
private int rowCount;
private int fixedWidth;
private int fixedHeight;
private boolean moveInViewport;
private boolean enableScale;
private float maxScale;
private float minScale;
public CurtainLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClipChildren(false);
setClipToPadding(false);
mCurtainViewContrainer = new CurtainViewContrainer(getContext());
addView(mCurtainViewContrainer);
initAttr(context, attrs);
mGestureHandler = new TouchEventHandler(getContext(), mCurtainViewContrainer);
//设置是否在窗口内移动
mGestureHandler.setKeepInViewport(moveInViewport);
mGestureHandler.setEnableScale(enableScale);
mGestureHandler.setMinScale(minScale);
mGestureHandler.setMaxScale(maxScale);
mCurtainViewContrainer.setHorizontalSpacing(horizontalSpacing);
mCurtainViewContrainer.setVerticalSpacing(verticalSpacing);
mCurtainViewContrainer.setRowCount(rowCount);
mCurtainViewContrainer.setFixedWidth(fixedWidth);
mCurtainViewContrainer.setFixedHeight(fixedHeight);
if (fixedWidth > 0 || fixedHeight > 0) {
if (fixedWidth > 0) {
mCurtainViewContrainer.setLayoutDirectionVertical(fixedWidth);
} else {
mCurtainViewContrainer.setLayoutDirectionHorizontal(fixedHeight);
}
}
}
/**
* 获取自定义特点
*/
private void initAttr(Context context, AttributeSet attrs) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.CurtainLayout);
this.horizontalSpacing = mTypedArray.getDimensionPixelSize(R.styleable.CurtainLayout_horizontalSpacing, 20);
this.verticalSpacing = mTypedArray.getDimensionPixelSize(R.styleable.CurtainLayout_verticalSpacing, 20);
this.rowCount = mTypedArray.getInteger(R.styleable.CurtainLayout_rowCount, 6);
this.fixedWidth = mTypedArray.getDimensionPixelOffset(R.styleable.CurtainLayout_fixedWidth, 150);
this.fixedHeight = mTypedArray.getDimensionPixelSize(R.styleable.CurtainLayout_fixedHeight, 180);
this.moveInViewport = mTypedArray.getBoolean(R.styleable.CurtainLayout_moveInViewport, false);
this.enableScale = mTypedArray.getBoolean(R.styleable.CurtainLayout_enableScale, true);
this.minScale = mTypedArray.getFloat(R.styleable.CurtainLayout_minScale, 0.7f);
this.maxScale = mTypedArray.getFloat(R.styleable.CurtainLayout_maxScale, 1.5f);
mTypedArray.recycle();
}
...
public void setMoveInViewportInViewport(boolean moveInViewport) {
this.moveInViewport = moveInViewport;
mGestureHandler.setKeepInViewport(moveInViewport);
}
public void setEnableScale(boolean enableScale) {
this.enableScale = enableScale;
mGestureHandler.setEnableScale(enableScale);
}
public void setMinScale(float minScale) {
this.minScale = minScale;
mGestureHandler.setMinScale(minScale);
}
public void setMaxScale(float maxScale) {
this.maxScale = maxScale;
mGestureHandler.setMaxScale(maxScale);
}
public void setHorizontalSpacing(int horizontalSpacing) {
mCurtainViewContrainer.setHorizontalSpacing(horizontalSpacing);
}
public void setVerticalSpacing(int verticalSpacing) {
mCurtainViewContrainer.setVerticalSpacing(verticalSpacing);
}
public void setRowCount(int rowCount) {
mCurtainViewContrainer.setRowCount(rowCount);
}
public void setFixedWidth(int fixedWidth) {
mCurtainViewContrainer.setLayoutDirectionVertical(fixedWidth);
}
public void setFixedHeight(int fixedHeight) {
mCurtainViewContrainer.setLayoutDirectionHorizontal(fixedHeight);
}
然后在布局容器与事情处理类中做对应的赋值操作即可。
怎么运用?
<CurtainLayout
android:id="@+id/curtain_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:enableScale="true"
app:fixedWidth="150dp"
app:horizontalSpacing="10dp"
app:maxScale="1.5"
app:minScale="0.8"
app:moveInViewport="true"
app:rowCount="6"
app:verticalSpacing="10dp">
</CurtainLayout>
假如在xml中设置过 fixedWidth 或许 fixedHeight ,那么在 Activity 中也能够不设置 LayoutManager 了。
val list = listOf<String>( ... )
val adapter = Viewgroup6Adapter(list)
val curtainView = findViewById<CurtainLayout>(R.id.curtain_view)
curtainView.adapter = adapter
终究作用:
跋文
关于 ViewGroup 的丈量与布局与事情,咱们现已从易到难温习了四期了,相信同学应该是能掌握了。
话说到里就应该到了结束时间,关于自定义View与自定义ViewGroup的温习与回忆就到此告一段落了,关于市面上能见到的一些布局作用,基本上能经过自定义ViewGroup与自定义View来结束。其实很早就想结束了,因为感觉这些东西有一点过于根底了,如同我们都不是很有爱好看这些根底的东西,
自定义View能够很方便的做自定义的绘制与自身与内部的一些移动,而关于一些多View移动的特效,咱们就算用自定义View难以结束或结束的比较杂乱的话,也能运用Behivor或许MotionLayot 来结束,当然这便是另一个篇章了。
假如有爱好也能够看看我之前的 Behivor 文章 【传送门】 或许 MotionLayot 的文章,【传送门】。
同时也能够搜索与翻看之前的文章哦。
本文的代码均能够在我的Kotlin测试项目中看到,【传送门】。你也能够关注我的这个Kotlin项目,我有时间都会持续更新。
关于本文的全屏滑动作用,我也会开源传到 MavenCentral 供我们依靠运用,【传送门】
运用:Gradle中直接依靠即可:
implementation “com.gitee.newki123456:curtain_layout:1.0.0”
好了,假如相似的作用有更多的更好的其他方法,也期望我们能评论区沟通一下。
常规,我如有解说不到位或讹夺的当地,期望同学们能够指出。
假如感觉本文对你有一点点的协助,还望你能点赞
支撑一下,你的支撑是我最大的动力。
哎,找图片都找了接近一个小时,假如我们想要对应的图片也能够去项目中拿哦!
Ok,这一期就此结束。