RecycleView自定义LayoutManager(一)

1.纵向Layoutmanager(VerticalLayoutManager)

  1. 先写一个类继承Layoutmanager,默认要实现generateDefaultLayoutParams方法,一般没有要修改itemview布局参数的话,默认就按下面来写
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }

2.如果只重写这个方法的话,我们运行起来会发现什么都没有显示,这是因为所有的itemView的布局都是在onLayoutChildren方法中,所以我们要重写这个方法来布局所有的itemview

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return;
        }
        //摆放可见的itemview
        int temp = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View child = recycler.getViewForPosition(i);
            measureChildWithMargins(child, 0, 0);
            int itemWidth = getDecoratedMeasuredWidth(child);
            int itemHeight = getDecoratedMeasuredHeight(child);
            addView(child);
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, getPaddingLeft(), temp, itemWidth, temp + itemHeight);
            temp += itemHeight;
        }  
         mTotalHeight = Math.max(getVerticalSpace(), temp);
    }
  • getItemCount()==0的话代表数据为0我们直接return
  • 遍历RecycleView所有的条目
  • 每个条目的宽高都一样,所以每个条目的left和right一样,只需要将每个条目的高度累加就可以将所有条目纵向排列起来
  • 必须先测量再拿宽高否则拿不到


    1.jpg

3.你会发现此时不可以上下滑动,想要上下滑动还需要重写下面二个方法

    @Override
    public boolean canScrollVertically() {
        return true;
    }
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    offsetChildrenVertical(-dy);
    return dy;
  }
  • canScrollVertically方法返回ture代表可以竖直滑动
  • scrollVerticallyBy方法中 dy>0代表上滑,dy<0代表下滑
  • 用offsetChildrenVertical方法来移动所有的item
1.gif

4.你会发现itemview可以拖出屏幕之外,所以要对拖动范围进行限制,上滑不能超出RecycleView高度,下滑不能滑动到0以下

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return dy;
        }
        int travel = dy;
        //限定拖动范围
        if (mTotalMoveY + travel < 0) {
            travel = -mTotalMoveY;
        } else if (mTotalMoveY + travel > mTotalHeight - getVerticalSpace()) {
            travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
        }
        mTotalMoveY += travel;
        offsetChildrenVertical(-travel);
        return travel;
    }

2.纵向复用(VerticalLayoutManagerRecycled)

1.上面效果看上去和LinerLayoutManager没有什么区别,但是我们在Adapter的onCreateViewHolder和onBindViewHolder方法中打印Log会发现有多少条目就同时调用了多少次onCreateViewHolder和onBindViewHolder方法,onCreateViewHolder执行一次代表创建了一个itemview,这就代表没有复用itemview,在数据量大的情况下就会发生anr异常

2.复用用到的几个重要的方法:

  • public void detachAndScrapAttachedViews(Recycler recycler)
    仅用于onLayoutChildren中,在布局前,将所有在显示的HolderView从RecyclerView中剥离,将其放在mAttachedScrap中,以供重新布局时使用

  • View view = recycler.getViewForPosition(position)
    用于向RecyclerView申请一个HolderView,这个HolderView是从缓存池子拿的,正是这个函数能为我们实现复用。

  • removeAndRecycleView(child, recycler)
    这个函数仅用于滚动的时候,在滚动时,我们需要把滚出屏幕的HolderView标记为Removed,这个函数的作用就是把已经不需要的HolderView标记为Removed。在我们标记为Removed以为,会把这个HolderView移到mCachedViews中,如果mCachedViews已满,就利用先进先出原则,将mCachedViews中老的holderView移到mRecyclerPool中,然后再把新的HolderView加入到mCachedViews中。

  • int getPosition(View view)
    这个函数用于得到某个View在Adapter中的索引位置,我们经常将它与getChildAt(int position)联合使用,得到某个当前屏幕上在显示的View在Adapter中的位置

3.具体代码

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return;
        }
        detachAndScrapAttachedViews(recycler);
        View view = recycler.getViewForPosition(0);
        measureChildWithMargins(view, 0, 0);
        int itemWidth = getDecoratedMeasuredWidth(view);
        int itemHeight = getDecoratedMeasuredHeight(view);
        //屏幕上可见的itemView个数
        int visible = getVerticalSpace() / itemHeight;

        int temp = 0;
        for (int i = 0; i < getItemCount(); i++) {
            Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
            mRectArray.put(i, rect);
            temp += itemHeight;
        }
        //摆放可见的itemview
        for (int i = 0; i < visible; i++) {
            Rect rect = mRectArray.get(i);
            View child = recycler.getViewForPosition(i);
            addView(child);
            measureChildWithMargins(child, 0, 0);
            layoutDecorated(child, rect.left, rect.top, rect.right, rect.bottom);
        }

        mTotalHeight = Math.max(getVerticalSpace(), temp);
    }
  • 先调用detachAndScrapAttachedViews方法把所有可见的itemview剥离
  • 定义一个集合存储Rect,每个Rect都记录了每个itemView的位置
  • 一屏中能放几个item就获取几个HolderView,撑满初始化的一屏即可,不要多创建,visible 代表可见的itemView个数
 //dy>0  dy<0
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return dy;
        }
        int travel = dy;
        if (travel + mTotalMoveY < 0) {
            travel = -mTotalMoveY;
        } else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
            travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
        }
        //回收不在屏幕内的itemview
      for (int i = getChildCount()-1; i >=0; i--) {
            View view = getChildAt(i);
            //下滑回收上面移出屏幕的itemview
            if (getDecoratedBottom(view) - travel < 0) {
                removeAndRecycleView(view, recycler);
                //上滑回收下面移出屏幕的itemview
            } else if (getDecoratedTop(view) - travel > getVerticalSpace()) {
                removeAndRecycleView(view, recycler);
            }
        }
        //获取当前可见的屏幕区域
        Rect visibleRect = getVisibleRect(travel);
        //移动时将回收的itemview从缓存中取出来
        if (travel > 0) {
            //上滑即将出来的条目的索引
            int next = getPosition(getChildAt(getChildCount() - 1)) + 1;
            for (int i = next; i < getItemCount(); i++) {
                insertView(recycler, visibleRect, i, false);
            }
        } else {
            //下滑即将出来的条目的索引
            int last = getPosition(getChildAt(0)) - 1;
            for (int i = last; i >= 0; i--) {
                insertView(recycler, visibleRect, i, true);
            }
        }

        mTotalMoveY += travel;
        offsetChildrenVertical(-travel);
        return travel;
    }
    private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
        Rect rect = mRectArray.get(pos);
        if (Rect.intersects(visibleRect, rect)) {
            View child = recycler.getViewForPosition(pos);
            if (flag) {
                addView(child, 0);
            } else {
                addView(child);
            }
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
                    rect.right, rect.bottom - mTotalMoveY);
        }
    }

   private int getVerticalSpace() {
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }
    private Rect getVisibleRect(int travel) {
        return new Rect(getPaddingLeft(), mTotalMoveY + travel,
                getWidth(), mTotalMoveY + travel + getVerticalSpace());
    }
  • 回收不在屏幕内的itemview中遍历屏幕内的itemview,getDecoratedBottom(view) - travel < 0代表上滑屏幕内的第一个itemview即将<0超出屏幕所以remove掉
  • getDecoratedTop(view) - travel > getVerticalSpace()代表下滑屏幕内的最后一个itemview即将超出屏幕所以remove掉
  • int next = getPosition(getChildAt(getChildCount() - 1)) + 1代表上滑时即将出现的条目,从这开始遍历把即将出现的条目都添加进来
  • int last = getPosition(getChildAt(0)) - 1代表下滑时即将出现的条目


    2.gif
  • 此时打Log可以看到在调用了几次onCreateViewHolder以后都是调用onBindViewHolder代表实现了复用
12-19 11:57:51.996 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+49
12-19 11:57:51.997 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+50
12-19 11:57:51.998 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+51
12-19 11:57:52.012 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+52
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+53
12-19 11:57:52.013 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+54
12-19 11:57:52.029 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+55
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+56
12-19 11:57:52.030 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+57
12-19 11:57:52.045 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+58
12-19 11:57:52.046 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+59
12-19 11:57:52.048 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+60
12-19 11:57:52.061 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+61
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+62
12-19 11:57:52.062 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+63
12-19 11:57:52.078 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+64
12-19 11:57:52.079 19035-19035/com.chinamall21.mobile.study E/Study=: onBindViewHolder+65

3.纵向Layoutmanager带动画(VerticalLayoutManagerAnim)

1.使用offsetChildrenVertical(-travel)函数来移动屏幕中所有item。这种方法仅适用于每个item,在移动时,没有特殊效果的情况,当我们在移动item时,同时需要改变item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)来移是不行的。针对这种情况,我们就只有使用第二种方法来实现回收复用了。
2.我们主要替换掉移动item所用的offsetChildrenVertical(-travel);函数,既然要将它弃用,那我们就只能自己布局每个item了。
3.具体代码

  • 定义一个集合来存储已经布局的itemview的position
 //是否在当前屏幕的itemView
    private SparseBooleanArray mAttachItems = new SparseBooleanArray();
  • 在onLayoutChildren中进行默认全部为false即没有进行过布局
    for (int i = 0; i < getItemCount(); i++) {
            Rect rect = new Rect(getPaddingLeft(), temp, itemWidth, itemHeight + temp);
            mRectArray.put(i, rect);
            mAttachItems.put(i, false);
            temp += itemHeight;
        }
 //dy>0  dy<0
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return dy;
        }
        int travel = dy;
        if (travel + mTotalMoveY < 0) {
            travel = -mTotalMoveY;
        } else if (travel + mTotalMoveY > mTotalHeight - getVerticalSpace()) {
            travel = mTotalHeight - getVerticalSpace() - mTotalMoveY;
        }

        mTotalMoveY += travel;
        //获取当前可见的屏幕区域
        Rect visibleRect = getVisibleRect();
        //回收不在屏幕内的itemview
        for (int i = getChildCount()-1; i >= 0; i--) {
            View view = getChildAt(i);
            int position = getPosition(view);
            Rect rect = mRectArray.get(position);
            if (Rect.intersects(visibleRect, rect)) {
                layoutDecoratedWithMargins(view, rect.left, rect.top - mTotalMoveY, rect.right, rect.bottom - mTotalMoveY);
                mAttachItems.put(position, true);
                view.setRotationY(view.getRotationY() + 1);
            } else {
                removeAndRecycleView(view, recycler);
                mAttachItems.put(position, false);
            }
        }
        int next = getPosition(getChildAt(getChildCount() - 1));
        int last = getPosition(getChildAt(0));
        //移动时将回收的itemview从缓存中取出来
        if (travel > 0) {
            //上滑即将出来的条目的索引
            for (int i = next; i < getItemCount(); i++) {
                insertView(recycler, visibleRect, i, false);
            }
        } else {
            //下滑即将出来的条目的索引
            for (int i = last; i >= 0; i--) {
                insertView(recycler, visibleRect, i, true);
            }
        }
        return travel;
    }
    private void insertView(RecyclerView.Recycler recycler, Rect visibleRect, int pos, boolean flag) {
        Rect rect = mRectArray.get(pos);
        if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) {
            View child = recycler.getViewForPosition(pos);
            if (flag) {
                addView(child, 0);
            } else {
                addView(child);
            }
            measureChildWithMargins(child, 0, 0);
            layoutDecoratedWithMargins(child, rect.left, rect.top - mTotalMoveY,
                    rect.right, rect.bottom - mTotalMoveY);
            child.setRotationY(child.getRotationY() + 1);
            mAttachItems.put(pos, true);
        }
    }
  • 我们先进行对当前屏幕的itemview进行遍历如果在屏幕内直接布局否则就remove掉然后把它们的状态都存起来,在屏幕内的我们给它设置了一个 view.setRotationY(view.getRotationY() + 1)动画
  • 下面同样进行即将出现的条目进行添加 if (Rect.intersects(visibleRect, rect) && !mAttachItems.get(pos)) ,如果在屏幕内并且没有进行过布局的就add进来,然后进行状态存储和设置动画


    3.gif

4.横向滑动HorizontalLayoutmanager与流失布局效果FlowLayoutmanager

  • 复用未带动画


    4.gif

    6.gif

    -复用带动画


    5.gif

    7.gif
  • 横向的LayoutManager与纵向的大体一致,只不过布局摆放有所不同,这里不在赘述
  • FlowLayoutManager复用和滑动都和VerticalLayoutManager的scrollVerticallyBy方法一致只是onLayoutChildren有所不同
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0) {
            return;
        }
        detachAndScrapAttachedViews(recycler);
        int tempHeight = mDefaultMargin;
        int tempWidth = mDefaultMargin;
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int itemWidth = getDecoratedMeasuredWidth(view);
            int itemHeight = getDecoratedMeasuredHeight(view);
            //超出屏幕宽度换行
            if (tempWidth + itemWidth >= getHorizontalSpace()) {
                tempWidth = mDefaultMargin;
                tempHeight += itemHeight + mDefaultMargin;
            }
            layoutDecoratedWithMargins(view, tempWidth, tempHeight, itemWidth + tempWidth, tempHeight + itemHeight);
            tempWidth += itemWidth + mDefaultMargin;
        }
        mTotalHeight = Math.max(tempHeight, getVerticalSpace());
    }
  • 遍历所有的item如果宽度加起来超出屏幕宽度换行,height累加,width重置这样就可以实现效果

源码地址https://github.com/digtal/recycleview-study

本篇内容参考于https://blog.csdn.net/harvic880925/article/details/84979161

你可能感兴趣的:(RecycleView自定义LayoutManager(一))