0
点赞
收藏
分享

微信扫一扫

深入理解RecyclerView

14年Google发布了万众期待的Android 5.0 。随之而来的还有新的设计方案 Material Design。为了在5.0以下的版本中也兼容这种设计方案, Google在新的support包中放出了大量控件,这其中就包括我们今天要讲的RecyclerView。

这篇文章并不会讲RecyclerView怎么用,而且通过分析RecyclerView内部的运行机制,以便于能恰当、正确的使用RecyclerView。


先看看RecyclerView都包含什么

那这么厉害的RecyclerView代码都有多复杂啊。
错了,一个好汉三个帮,弱蜀还有五虎上将呢,RecyclerView这么好用的控件怎么可能单枪匹马逞英雄?

接下来我会一一介绍它们

LayoutManager

LayoutManager作为布局先锋,负责RecyclerView内部Item的测量和布局。说白了就是,RecyclerView自己不再负责Measure、Layout,全权委托给LayoutManager来处理。这样做的好处就是职责清晰,开发者不但可以自由的使用列表、网格、瀑布流等常规的布局,还可以自定义LayoutManager来满足特殊的列表需求。比如:



上图中的这个复杂列表,在阿里系的APP上比较常见,比如优酷、天猫。大致结构是的:

public class Pager {
    List<Card> mCards;
    class Card {
        List<Item> mItems;
        class Item {
            public int mId;
            public String mName;
        }
    }
}

后端会返回List<Card> mCards 。其中每一个卡片,又是一个列表List<Item> mItems。这个时候LayoutManager就派上用场了,我们可以继承LinearLayoutManager,来处理每一个卡片如何布局,同时,我们需要卡片重的Item打平,这样就可以有效利用RecyclerView的缓存机制。在之后的系列文章中,我会详细解释。
阿里爸爸开源的复杂列表VLayout
这个是阿里开发的一个用于显示复杂列表的LayoutManager,有兴趣的可以看一眼。

如果支持,就需要先计算Adapter中所有item的大小,然后在计算RecyclerView自己的大小。整个过程比较消耗性能,迫不得已,不要使用。

item 添加、删除、大小变化都可能触发动画,举个例子,如果RecyclerView使用默认的动画,删除Position为0的Item,其余的Item就会整体向上移动。这个时候就需要知道,item的偏移量,只之后真正的Layout做准备。

真正的布局需要RecyclerView的大小,Item的起始位置,布局方向,每一次布局之后的偏移量。

如果想让我们RecyclerView滚动起来,就需要在LayoutManager来做特殊的处理。

自定义LayoutManger相对比较复杂。也不是我短短几句话就能讲清楚的,需要开发者不断的写Demo,查阅源码或者相关文章,才能完成一个完整的LayoutManger。下面介绍自定义LayoutManger必须知道的几个知识点:

public class DemoLayoutManager extends RecyclerView.LayoutManager {

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        //返回ItemView的默认大小
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    /**
     * 处理Item布局的问题
     */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

        // 第一步:移除当前界面中的item,并添加到回收站中
        detachAndScrapAttachedViews(recycler);

        int xOffset = 0;
        int yOffset = 0;

        // 第二步:把所有的Item放在他们应该放的位置
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i); // 从回收站中取出View
            addView(view);
            measureChildWithMargins(view, 0, 0); // 计算View的大小

            int width = getDecoratedMeasuredWidth(view);
            int height = getDecoratedMeasuredHeight(view);

            // 将view放置在正确的位置 (这个位置会收到ItemDecoration的影响)
            layoutDecorated(view, xOffset, yOffset, xOffset + width, yOffset + height);

            if (i % 6 == 5) {
                xOffset = 0;
                yOffset += height;
            } else {
                xOffset += width;
            }

        }

    }


    @Override
    public boolean canScrollVertically() {
        // 控制能否上下滚动
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 在滚动的时候移动view
        offsetChildrenVertical(-dy);
        return dy;
    }

Recycler

Recycler作为回收站,Item的回收和复用都是由Recycler来控制的。那他是如何来处理的呢?

我们来看上面这张图。图中展示来RecyclerView复用ItemView的机制。其中 #、1、2、3、4 分别代表:

ItemAnimator

ItemAnimator主要是处理动画。这个动画主要添加、删除、移动的动画。国内开发者,需要特别绚丽多彩的动画并不多。同样我也没有遇到这种需求。我认为默认的动画就还不错。
如果你真的想自定义ItemAnimator。我推荐Github的一个开源库,大家可以参考参考。我之后有时间,也会详细介绍这方面的知识。
https://github.com/wasabeef/recyclerview-animators
但是需要注意,一个Item的操作,可能会触发多个动画,比如,在中间位置插入一条数据,这个时候,就会触发插入的动画,和原来这个位置以后的Item都向下移动的动画。

DiffUtil

DiffUtil是最近版本中推出的一个工具。主要是帮助RecyclerView提升刷新效率的问题。我们举一个例子来说这个问题。

这样的搜索功能是很常见的。每次输入不同的文字,都要给出该文字相对应的搜索热词推荐。如果直接使用notifyDataSetChanged(),就会导致整个RecyclerView发生RequestLayout。我们都知道RequsetLayout会引起整个View树重新遍历一边Measure和Layout。这样非常消耗性能。而且,RecyclerView重新加载时,只会从RecyclerViewPool中拿缓存的Item。RecyclerViewPool默认只会缓存5个Item。剩下的Item都需要重新走Create和inflate。之后他们还要重写计算宽高,重新计算布局。这个过程非常耗时。

为了提供性能,我们就可以使用DiffUtil来对比两组数据,得到数组A切换到数组B的最少移动步骤。

“寻找diff”这件事,被抽象成了“寻找图的路径”了。那么,“最短的直观的”diff对应的路径有什么特点呢?

其实Myers算法是一个典型的”动态规划“算法,也就是说,父问题的求解归结为子问题的求解。要知道d=5时所有k对应的最优坐标,必须先要知道d=4时所有k对应的最优坐标,要知道d=4时的答案,必须先求解d=3,以此类推,和01背包问题很是相似。

        public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
            dispatchUpdatesTo(new ListUpdateCallback() {
                @Override
                public void onInserted(int position, int count) {
                    adapter.notifyItemRangeInserted(position, count);
                }

                @Override
                public void onRemoved(int position, int count) {
                    adapter.notifyItemRangeRemoved(position, count);
                }

                @Override
                public void onMoved(int fromPosition, int toPosition) {
                    adapter.notifyItemMoved(fromPosition, toPosition);
                }

                @Override
                public void onChanged(int position, int count, Object payload) {
                    adapter.notifyItemRangeChanged(position, count, payload);
                }
            });
        }

在使用DiffUtil得到变化之后,我们可以调用RecyclerView的局部刷新机制。这样不需要RequestLayout。刷新效率非常高。
那DiffUtil的对比数据的效率怎么样呢。
这里有一组官方的数据:

ItemDecoration

ItemDecoration 很简单。就是RecyclerView的装饰品。你可以想象RecyclerView是个小姑娘。ItemDecoration就是小姑娘的化妆品。

        public void onDraw(Canvas c, RecyclerView parent, State state) {
           //在画Item之前。
        }
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            //在画Item之后
        }

public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            // 设置边距
            outRect.set(left, top, right, bottom);
        }

值得注意的事,在给Item画装饰品的时候,一定要注意Item本身的位置。

       @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            if (state.isMeasuring()) return;
   
            c.save();
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(child);
                // 获取你的标签中显示的数量
                int count = parent.getAdapter().getItemTag();
                if (count == 0) continue;

                String strCount = String.valueOf(count);
                float textWidth = 0;
                if (count >= 10) {
                    textWidth = mTextPaint.measureText(strCount) - mRadius;
                }
                mRoundRectF.left = child.getRight() - textWidth - mRadius - mLeft;
                mRoundRectF.top = child.getTop() - mRadius + mTop;
                mRoundRectF.right = child.getRight() + mRadius - mLeft;
                mRoundRectF.bottom = child.getTop() + mRadius + mTop;
                c.drawRoundRect(mRoundRectF, mRadius, mRadius, mPopPaint);
                Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
                float baseline = child.getTop() - ((fontMetrics.descent + fontMetrics.ascent) / 2);
                c.drawText(strCount, mRoundRectF.left + mRoundRectF.width() / 2, baseline + mTop, mTextPaint);
            }
            c.restore();
        }

SnapHelper

有关SnapHelper的 请看这里。我之后会讲。
https://github.com/rubensousa/RecyclerViewSnap

ItemTouchHelper

有关ItemTouchHelper的 请看这里。我之后会讲。
https://github.com/iPaulPro/Android-ItemTouchHelper-Demo

AdapterHelper

以后补全

ChildHelper

在发生移除动画时,对于ViewGroup来说,由于动画还在发生,所以View并没有被真正的从ViewGroup中移除。而对于LayoutManager来说,这个View已经被移除,需要对他做回收处理。


这个时候,LayoutManager 操作View的时候,比如getChildAt()。这个方法并不是真正冲ViewGroup中获取,而是从ChildHelper维护的View队列中获取。


使用RecyclerView的注意点

1.如何RecyclerView的宽高不随着内容的变化而变化,就可以使用如下方法来提高性能

mRecyclerView.setHasFixedSize(true);

2.如果想提高RecyclerView的滑动流畅性,可以适度增加Cache的大小,默认大小是2。但是也不能太大,如果太大,会影响初始化的效率。

mRecyclerView. setItemViewCacheSize(5);

3.如果多个RecyclerView显示的Item一样。比如:

比如这种情况,就可以使用公告缓存池。

int type0 = 0;
int type1 = 1;
int type2 = 2;

RecyclerViewPool mPool = new RecyclerViewPool();
mPool.setMaxRecycledViews(type0, 10);
mPool.setMaxRecycledViews(type1, 10);
mPool.setMaxRecycledViews(type2, 10);

3.有时候我们会在Item上添加一些手势处理,比如最常见的侧滑删除,Item拖拽等等。在有些特殊的手机上,你会发现拖拽不灵敏,这个时候就可以用

mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

4.RecyclerView 默认是带Item动画的。如果你不需要动画,或者性能要求严格,可以关闭动画。

mRecyclerView.setItemAnimator(null);

5.RecyclerView如果要现实图片,可以在惯性滚动的时候暂停图片加载,这样可以提升流畅度。

mRecyclerView.setOnFlingListener();

参考资料
1.http://v.youku.com/v_show/id_XMTU4MTQ1ODg2NA==.html?f=27314446&debug=flv
2.https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
3.com.android.support:recyclerview-v7:25.3.1

举报

相关推荐

0 条评论