范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

Android自定义ViewGroup嵌套与交互实战,幕布全屏滚动效果

  自定义 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 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 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 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 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 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> 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 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 totalHeights = new ArrayList<>();         for (int i = 0; i < columnAllHeight.size(); i++) {             List 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
  定义属性如下:                                                                                                                                                                         复制代码
  取出属性并对容器布局与触摸处理器做赋值的操作:  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);     }  复制代码
  然后在布局容器与事件处理类中做对应的赋值操作即可。
  如何使用?            复制代码
  如果在xml中设置过 fixedWidth 或者 fixedHeight ,那么在 Activity 中也可以不设置 LayoutManager 了。      val list = listOf( ... )      val adapter = Viewgroup6Adapter(list)      val curtainView = findViewById(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,这一期就此完结。

东方观野看北美(加拿大网络霸主Rogers断网事故的前因与背后)加拿大七天传媒7月15日刊发文章,对加拿大网络运营商Rogers的大面积断网事故,进行了深度报道7月8日一大早,加拿大三大通讯运营商之一的Rogers公司,出现大面积设备故障。影响毛氏族谱记载,毛主席原配并非杨开慧,斯诺证实是位罗姓女子毛主席的故乡韶山冲引言毛主席的性格和人格受到母亲的影响,知恩图报,不忘初心,任何对自己有帮助的人,滴水之恩涌泉相报。新中国成立后,毛主席日理万机难以回乡看望乡亲们,便让自己的大儿子4S店引诱14岁女孩发生关系后续车内女子与事件无关,她已满18岁网上常说十辆凯迪拉克九渣。在高德地图发布的七大汽车品牌车主行为报告中,大家发现凯迪拉克的车主偏爱洗浴场所,便有了上述的结论。虽然这种说法并没有根据,但大家对凯迪拉克的车主,总会有一买冰箱,海尔和美的哪个好?过来人差别很大,别选错了很多人买冰箱,就认准一个东西品牌。其实这样也没错,因为好的品牌代表好的质量和品控。大品牌的冰箱有很多,比如说海尔,美的,容声,松下等等。这些品牌放在一起,难免会有比较,到底哪个更好网传陈某志会被取保候审,还会减刑,真相是啥?很多人只知道是陈某志一人引发的唐山打人事件,但是很多人并不知道,在陈某志的背后其实还有很多普通人想都不敢想的案中案。在说陈某志背后的案中案之前,我们不得不来说两件最近很多人都非常担拜登中东之行草草收场,沙特拒绝石油增产,孤立中俄也未有响应据新华网报道,美国总统拜登近日结束了任内的首次中东之行。出访前拜登曾高调宣称此行将翻开美国与中东国家间的新篇章。然而结果却是接连碰壁在以色列访问毫无成果,在巴勒斯坦遭民众抗议,在沙中东新王地表最强85后沙特王储小萨勒曼的崛起拜登的中东之行以一个极其平淡的结局收场了。现在回顾一下,拜登的这趟中东之行不仅平淡,而且处处充满了尴尬。拜登曾多次公开批评沙特的人权问题,甚至将沙特称为贱民国家,沙特王储小萨勒曼被2022年国家助学贷款开始受理多项政策出台缓解还款压力国家助学贷款是由政府主导财政贴息,专门帮助高校家庭经济困难学生解决学习期间学费住宿费弥补生活费的一种信用贷款。这两天,国家开发银行开始受理2022年度生源地信用助学贷款,这也意味着公司3个月没发工资,为什么同事不仅没走,还每天都来上班?粉丝提问我们公司已经3个月没发工资了。问老板,他就说下个月一起发。同事间都在传,公司的资金链出了问题。既然公司资金链都出问题了,为什么大家还不赶紧走,而是每天来公司上班?大家没走都湖北小伙李哨兵为救白血病女友花光40万,女友病好后却嫁给别人2002年底,湖北小伙李哨兵的女友得了白血病,为了女友的医疗费,他毅然将自己的饭馆和车子卖掉,最终筹得40万。但康复后的女友却抛弃了李哨兵,转投他人怀抱,和他人结婚生子。这让李哨兵天津新增42!一区幼儿园教培机构停课三天广西北海紧急搭建气膜实验室点蓝字关注,不迷路天津7月18日0至18时新增本土确诊病例4例新增本土无症状感染者2例7月18日,天津召开疫情防控第185场新闻发布会。发布会上,天津市疾病预防控制中心主任天津市卫
阿里云ADB基于Hudi构建Lakehouse的实践导读大家好,我是来自阿里云数据库的李少锋,现在主要专注于ADBHudiSpark的研发以及产品化,今天非常高兴能够借这个机会和大家分享下阿里云ADB基于ApacheHudi构建La华为手机强势回归!余承东说到做到,2023年王者归来!芯片规则被修改之后,华为的很多业务都受到了很多影响,其中手机业务受到了冲击最大,过去华为手机原本是全球第二中国第一,年销量超过2亿台,但现在华为手机一年的销量只有3000万台了。华一天折叠200次寿命可达5年!FindN2开卖,人气火爆的原因找到了近两年,折叠屏手机可谓风头正劲,各大厂商也不断加紧发力,推出一款又一款爆火的机型。只不过依然有部分消费者,对折叠屏手机持观望态度,究其原因是他们对于这类产品的耐用性还是存在不少的担vivo力拼年轻市场!中端机年终火热厮杀,明年谁会率先领涨华夏时报(www。chinatimes。net。cn)记者卢晓北京报道年底也是手机厂商密集发新机的时刻。除了旗舰机战场火药味十足,两千元左右价位的中端机也在年终火热起来。近日,先是入手华为Mate50系列三个月,对这些黑科技使用感受不吐不快在当前的高端手机市场,集实力与创新技术于一身的华为始终是国人心中不可超越的存在。今年9月份华为Mate50系列震撼上市,看过发布会的系列功能介绍后,我果断入手了全新一代华为MateTCL芯片即将崛起,全面布局半导体产业链,让电视从芯出发导语在21世纪00年代,当时中国在科技领域面临着两个问题缺芯少屏。如今,TCL华星已成为全球第二大屏幕供应厂商,成功解决了国内少屏的问题。而现在,TCL即将向下一个目标进发,那就是IQOOneo7竞速版官宣定档,与k60争夺旗舰捍门员?众所周知,VIVO旗下子品牌IQOO的定位是性价比品牌,其定位是游戏电竞手机。IQOO的游戏手机的调教在业内是有目共睹的。此前IQOO已经发布了neo7的标准版,标准版搭载的是天玑十三香直降1100元,加速退场,现在到底买iphone13还是14呢?不得不说在智能手机市场,目前能与苹果相较量的厂商少之又少,虽然三星是全球手机出货量第一的厂商,但是依然无法成功地将苹果用户传华为上新用户,这不仅仅是品牌上的过渡,而是整套生态环境和慈禧的陵墓上为何会寸草不生呢?在古代,历代皇帝将相都要在生前给自己选块儿风水宝地作为墓地。而慈禧太后作为晚清实际上的统治者,自然也不例外。自己是个很迷信的人,他相信人死之后会有来世,所以人死后居住的墓地一定要豪棺材里怎么躺着大活人?阜新公安剿灭宝珠营子村明佛道的故事头条创作挑战赛1950年秋的某天,辽西省阜新县宝珠营子村民兵巡逻归来后正坐在村委大院休息。跟着村公安委员韩广田去另一方向巡逻的一个民兵风风火火地跑进了大院,上气不接下气地说快!快!通俗的讲述八王之乱历史上的八王之乱,都是拜晋武帝司马炎所赐,不是他用了自己的老岳父主持大局,不是他亲手选定的太子妃贾南风,不是他大搞分封诸王,估计大一统的晋朝一定会来到,老百姓肯定会有好多年的幸福生