Android 进阶学习(七) RecyclerView 学习 (一) 自义定LayoutManager

在学习RecyclerView 之前根本没有想到RecyclerView 学习起来会消耗我这么多时间,从最开始看到开始写这篇文章大约用了一周的时间吧,期间看了很多博客,但是很多人写的东西我拿过来用根本不能达到我想要的结果,虽然我现在看的androidx 中关于RecyclerView 的源码,但是我觉得差别应该没有那么大,导致在写demo的过程中饶了很多弯路,在这里只是调侃一下为什么大部分人写帖子都是粘贴过来的,废话不多说了,进入正题

自定义 LayoutManager 中一些重要的方法

onLayoutChildren

onLayoutChildren 是 LayoutManager 测量和布局child 的入口,如果调用 manager 的requestLayout的方法,这个方法也会执行,所以这个方法中不能还原属性,

getItemCount

getItemCount 获取的item 的个数就是Adapter 中返回的个数,

getChildCount

getChildCount 是recyclerView 中可见的item 的数量,即没有被放进缓存的item 的数量,

image.png

从这个图片我们看到 ,如果我们遍历getChildCount ,那么我们是从可见列表中开始遍历的,他的position 也就是在adapter中的position 和 getChildCout 中的position 并不一定相同

getChildAt

获取屏幕中的第一个item,即getChildCount 中的第一个item

measureChildWithMargins

测量child ,但是child 可能包含Divider,机会被计算在内,

getDecoratedMeasuredHeight getDecoratedMeasuredWidth

这两个方法可以在测量过后得到child 的宽高,如果child 的宽高是一样的,那么只需要测量一次我们就知道了所有child 的宽高

recycler.getViewForPosition

recycler.getViewForPosition会得到一个view, 他的入参是一个position,这个position 就是item在adapter 中的position,获取View 的过程是一个相对复杂的过程,他到底是如何工作的,我会在下一篇文章说,

addView

addView 会将view 添加到 屏幕可见的item 列表中

layoutDecoratedWithMargins layoutDecorated

这两个方法是绘制 child 的方法, 可以让view 显示在recyclerview中

canScrollHorizontally canScrollVertically

这两个方法从字面意思的意思就能看出来,他是一个开关方法,控制着manager 的 水平和垂直是否可以滑动

offsetChildrenHorizontal offsetChildrenVertical

这两个方法非常重要,控制recyclerView 的水平和垂直的偏移量,在 使用这两个方法让manager 的偏移量发生变化时,item 所在的位置,也需要相对偏移量发生变化,

image.png

detachAndScrapAttachedViews

detachAndScrapAttachedViews 这个方法控制的数据也是getChildCount 中的数据,也就是显示在屏幕上面的数据,暂时先Detach掉,放入到Scrap缓存中 ,在后续绘制的过程中会很快的匹配到

removeAndRecycleView

removeAndRecycleView 这个方法就是回收子view了,将它放入到缓存中, 具体放入到哪里我们下一篇分析

到了这里所有我了解的比较重要的方法已经都完成了,接下来我么进入正题,期间我会写一下我所遇到的坑,先上一下我们要实现的效果图,

GIF 2020-11-16 14-46-02.gif

我们先一点一点开始
继承 RecyclerView.LayoutManager ,我们必须实现 generateDefaultLayoutParams 这个方法

   @Override
   public RecyclerView.LayoutParams generateDefaultLayoutParams() {
       return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
   }

onLayoutChildren 作为入口方法,我们先简单的实现一下布局的排列

   @Override
   public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
       if (getItemCount() == 0 || state.isPreLayout()) {///没有数据或者是 执行动画期间
           return;
       }
       for(int i=0;i
image.png

方法很简单,根据测量的宽高水平排列这个child,这个就是我们实现的效果,但是不能滑动,上面我们介绍到如果想要滑动必须先打开滑动开关,

   @Override
   public boolean canScrollHorizontally() {
       return true;
   }

实现了这个方法后,我们重新运行还是不能滑动,我们接下来再继续实现scrollHorizontallyBy 这个方法,

@Override
   public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
       int index = dx;
       offsetChildrenHorizontal(index);
       return index;
   }

我们发现这时会滑出屏幕外,我们继续修正

   @Override
   public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
       int index = dx;
       if ((index + horizontal_offset) < 0) {///修正偏移量
           index = -horizontal_offset;
       } else if ((index + horizontal_offset) > getMaxScrollWidth()) {/////修正偏移量
           index = getMaxScrollWidth() - horizontal_offset;
       }
       horizontal_offset += index;
       offsetChildrenHorizontal(index);
       return index;
   }

写到这里如果不写回收的话就已经结束了,但是我们要实现滑出屏幕外回收,就需要继续对他改造,想要回收我们肯定不能直接把所有的view都测量,而是哪个view在屏幕上就测量哪个view,并把它添加到recyclerView中,并绘制出来,
这里我们先定义几个变量,

///水平偏移量,
private int horizontal_offset = 0;

控件是否在屏幕内,我么只需要根据控件水平偏移量,看看这个控件和偏移量的关系即可

   /**
    * 判断是否在屏幕外
    *
    * 控件的右边小于偏移量或者左边大于偏移量+ 控件的的宽度
    *   
    * @param width_offset 两个child 之间的距离
    * @param child_widht   view 的宽度  show_width 整个recyclerview 的宽度
    * @return
    */
   public boolean isOutOfRange(int position){
       int child_left = (position * (child_widht + width_offset) + first_child_margin);
       int child_right = ((position + 1) * (width_offset + child_widht) + first_child_margin);
       if ((child_right) < (horizontal_offset) || (child_left) > (horizontal_offset + show_width)) {
           return true;
       }
       return false;
   }

了解了这个我们在重新修改onLayoutChildren 这个方法

   @Override
   public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
       if (getItemCount() == 0 || state.isPreLayout()) {///没有数据或者是 执行动画期间
           return;
       }
       View child;
       if (getChildCount() == 0) {///屏幕上没有数据,
           child = recycler.getViewForPosition(0);
       } else {
           child = getChildAt(0); //执行requestLayout 后,屏幕上就有数据,所以执行的是这个方法
       }
       measureChildWithMargins(child, 0, 0);//测量child  由于所有child 的宽高都是一致的,
       child_height = getDecoratedMeasuredHeight(child);//或者child 的高度
       child_widht = getDecoratedMeasuredWidth(child);//获取child 的宽度
       first_child_margin = getWidth() / 2 - child_widht / 2;///前面预留的宽度  recyclerview 的宽度除以2 - child的宽度除以2
       show_width = getWidth();
       layoutItem(recycler, state,0);
   }

初始化了数据之后我们还要根据偏移量重新对child 进行add,并对getChildCout (recyclerview 所包含的数据) 遍历,回收那些滑出屏幕外的child

   /**
    * 根据偏移量 重新布局item
    * @param recycler
    * @param state
    * @param dx
    */
   private void layoutItem(RecyclerView.Recycler recycler, RecyclerView.State state,int dx) {
       if (state.isPreLayout()) {
           return;
       }
       detachAndScrapAttachedViews(recycler);///暂时先detach
       for (int i = 0; i < getItemCount(); i++) {
           if(isOutOfRange(i)){///如果在屏幕外
               continue;
           }
           View child = recycler.getViewForPosition(i);///或者到这个view
           measureChildWithMargins(child, 0, 0);///重新测量,否则view 在复用的时候即使执行了bindViewHolder,也没有数据
           addView(child);//重新add
           ///这个左边应该是减去偏移的左边
           int left = first_child_margin + (child_widht + width_offset) * i;
           setScanAnim(child, left);///设置缩放动画
           layoutDecoratedWithMargins(child, left - horizontal_offset, 0, left + child_widht - horizontal_offset, child_height);
       }

       for (int m = 0; m < getChildCount(); m++) {
           View child=getChildAt(m);
           int i = getPosition(child);
           if(isOutOfRange(i)){///如果在屏幕外面
               removeAndRecycleView(child,recycler);//在屏幕外则回收
           }
       }
   }

在滑动scrollHorizontallyBy这个方法时,也调用layoutItem方法

   @Override
   public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
       int index = dx;
       if ((index + horizontal_offset) < 0) {///修正偏移量
           index = -horizontal_offset;
       } else if ((index + horizontal_offset) > getMaxScrollWidth()) {/////修正偏移量
           index = getMaxScrollWidth() - horizontal_offset;
       }
       horizontal_offset += index;
//        ///通知控件水平移动  非常重要,要不然不会移动  该方法必须要放在   offsetChildrenHorizontal 之前,否则有问题,
       layoutItem(recycler, state,index);

       offsetChildrenHorizontal(index);
       return index;
   }

当然缩放的时机也在layoutItem里面

   /**
    * 缩放
    * @param child
    * @param left
    */
   public void setScanAnim(View child, int left) {
       int child_center = (left - horizontal_offset + left + child_widht - horizontal_offset) / 2;
       int parentCenter = getWidth() / 2;
       float scale;
       if (child_center > parentCenter) {
           scale = 1.0f - (1 - 0.8f) * ((child_center - parentCenter) / (parentCenter * 1.0f));
       } else {
           scale = 1.0f - (1 - 0.8f) * ((parentCenter - child_center) / (parentCenter * 1.0f));
       }
       child.setScaleX(scale);
       child.setScaleY(scale);
   }

关于在离开触摸的时候自动修正position ,我们需要借助 anim来实现, 下面我贴一下所有的代码,至于想要实现更复杂的方法,大家可以参考一个github上一些开源控件,即使不能使用,自己修改一下也肯定可以满足自身的需求

public class TsmRouteManager extends RecyclerView.LayoutManager {

   public TsmRouteManager() {
       horizontal_offset = 0;
   }

   /**
    * 最小的缩放
    */
   private float minScanSize = 0.8f;
   /**
    * 最大的缩放
    */
   private float maxScanSize = 1.2f;
   /**
    * 第一个控件的偏移量
    */
   private int first_child_margin;

   private int child_widht;
   private int child_height;

   /**
    * 第一个控件的偏移量
    */
   private int horizontal_offset = 0;


   private int width_offset = 25;


   /**
    * 可见的高度
    */
   private int show_width;
   private ValueAnimator selectAnimator;


   @Override
   public RecyclerView.LayoutParams generateDefaultLayoutParams() {
       return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
   }

   @Override
   public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
       if (getItemCount() == 0 || state.isPreLayout()) {///没有数据或者是 执行动画期间
           return;
       }
       View child;
       if (getChildCount() == 0) {///屏幕上没有数据,
           child = recycler.getViewForPosition(0);
       } else {
           child = getChildAt(0); //执行requestLayout 后,屏幕上就有数据,所以执行的是这个方法
       }
       measureChildWithMargins(child, 0, 0);//测量child  由于所有child 的宽高都是一致的,
       child_height = getDecoratedMeasuredHeight(child);//或者child 的高度
       child_widht = getDecoratedMeasuredWidth(child);//获取child 的宽度
       first_child_margin = getWidth() / 2 - child_widht / 2;///前面预留的宽度  recyclerview 的宽度除以2 - child的宽度除以2
       show_width = getWidth();
       layoutItem(recycler, state,0);
   }


   /**
    * 根据偏移量 重新布局item
    * @param recycler
    * @param state
    * @param dx
    */
   private void layoutItem(RecyclerView.Recycler recycler, RecyclerView.State state,int dx) {
       if (state.isPreLayout()) {
           return;
       }
       detachAndScrapAttachedViews(recycler);///暂时先detach
       for (int i = 0; i < getItemCount(); i++) {
           if(isOutOfRange(i)){///如果在屏幕外
               continue;
           }
           View child = recycler.getViewForPosition(i);///或者到这个view
           measureChildWithMargins(child, 0, 0);///重新测量,否则view 在复用的时候即使执行了bindViewHolder,也没有数据
           addView(child);//重新add
           ///这个左边应该是减去偏移的左边
           int left = first_child_margin + (child_widht + width_offset) * i;
           setScanAnim(child, left);///设置缩放动画
           layoutDecoratedWithMargins(child, left - horizontal_offset, 0, left + child_widht - horizontal_offset, child_height);
       }

       for (int m = 0; m < getChildCount(); m++) {
           View child=getChildAt(m);
           int i = getPosition(child);
           if(isOutOfRange(i)){///如果在屏幕外面
               removeAndRecycleView(child,recycler);//在屏幕外则回收
           }
       }
   }

   /**
    * 判断是否在屏幕外
    *
    * 控件的右边小于偏移量或者左边大于偏移量+ 控件的的宽度
    *
    * @param position
    * @return
    */
   public boolean isOutOfRange(int position){
       int child_left = (position * (child_widht + width_offset) + first_child_margin);
       int child_right = ((position + 1) * (width_offset + child_widht) + first_child_margin);
       if ((child_right) < (horizontal_offset) || (child_left) > (horizontal_offset + show_width)) {
           return true;
       }
       return false;
   }


   /**
    * 缩放
    * @param child
    * @param left
    */
   public void setScanAnim(View child, int left) {
       int child_center = (left - horizontal_offset + left + child_widht - horizontal_offset) / 2;
       int parentCenter = getWidth() / 2;
       float scale;
       if (child_center > parentCenter) {
           scale = 1.0f - (1 - 0.8f) * ((child_center - parentCenter) / (parentCenter * 1.0f));
       } else {
           scale = 1.0f - (1 - 0.8f) * ((parentCenter - child_center) / (parentCenter * 1.0f));
       }
       child.setScaleX(scale);
       child.setScaleY(scale);
   }

   @Override
   public boolean isAutoMeasureEnabled() {
       return true;
   }

   @Override
   public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
       int index = dx;
       if ((index + horizontal_offset) < 0) {///修正偏移量
           index = -horizontal_offset;
       } else if ((index + horizontal_offset) > getMaxScrollWidth()) {/////修正偏移量
           index = getMaxScrollWidth() - horizontal_offset;
       }
       horizontal_offset += index;
//        ///通知控件水平移动  非常重要,要不然不会移动  该方法必须要放在   offsetChildrenHorizontal 之前,否则有问题,
       layoutItem(recycler, state,index);

       offsetChildrenHorizontal(index);
       return index;
   }


   public int getMaxScrollWidth() {
       return getItemCount() * child_widht - getWidth() / 2 + first_child_margin - child_widht / 2 + width_offset * (getItemCount() + 1);
   }


   @Override
   public void onScrollStateChanged(int state) {
       super.onScrollStateChanged(state);
       switch (state) {
           case RecyclerView.SCROLL_STATE_DRAGGING:
               //当手指按下时,停止当前正在播放的动画
               cancelAnimator();
               break;
           case RecyclerView.SCROLL_STATE_IDLE:
               smoothScrollToPosition(findShouldSelectPosition());
               break;
       }
   }

   public void smoothScrollToPosition(int position) {
       if (position > -1 && position < getItemCount()) {
           startValueAnimator(position);
       }
   }


   public void cancelAnimator() {
       if (selectAnimator != null && (selectAnimator.isStarted() || selectAnimator.isRunning())) {
           selectAnimator.cancel();
       }
   }


   /**
    * 开启动画,
    * @param position
    */
   private void startValueAnimator(int position) {
       cancelAnimator();

       final float distance = getScrollToPositionOffset(position);
       long minDuration = 100;
       long maxDuration = 300;
       long duration;

       float distanceFraction = (Math.abs(distance) / (child_widht + width_offset));

       if (distance <= (child_widht + width_offset)) {
           duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction);
       } else {
           duration = (long) (maxDuration * distanceFraction);
       }
       selectAnimator = ValueAnimator.ofFloat(0.0f, distance);
       selectAnimator.setDuration(duration);
       selectAnimator.setInterpolator(new LinearInterpolator());
       final float startedOffset = horizontal_offset;
       selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator animation) {
               float value = (float) animation.getAnimatedValue();
               horizontal_offset = (int) (startedOffset + value);
               requestLayout();
           }
       });
       selectAnimator.start();
   }

   /**
    * 计算这个位置的时候 少减去 first_child_margin 后,正好让下一个view 处于屏幕中央
    *
    * @param position
    * @return
    */
   private float getScrollToPositionOffset(int position) {
       return position * (child_widht + width_offset) - Math.abs(horizontal_offset);
   }

   /**
    * 计算这个position的时候 少减去 first_child_margin 后,正好让下一个view 处于屏幕中央
    *
    * @return
    */
   private int findShouldSelectPosition() {
       if (getItemCount() == 0) {
           return -1;
       }
       int position = (int) (Math.abs(horizontal_offset) / (child_widht + width_offset));
       int remainder = (int) (Math.abs(horizontal_offset) % (child_widht + width_offset));
       // 超过一半,应当选中下一项
       if (remainder >= (child_widht + width_offset) / 2.0f) {
           if (position + 1 <= getItemCount() - 1) {
               return position + 1;
           }
       }
       return position;
   }


   @Override
   public boolean canScrollHorizontally() {
       return true;
   }


}

你可能感兴趣的:(Android 进阶学习(七) RecyclerView 学习 (一) 自义定LayoutManager)