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