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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,321评论 6 543
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,559评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,442评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,835评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,581评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,922评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,931评论 3 447
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,096评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,639评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,374评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,591评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,104评论 5 364
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,789评论 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,196评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,524评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,322评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,554评论 2 379

推荐阅读更多精彩内容