似曾相识的 RecyclerView
一.常用方法
RecyclerView 与 ListView、GridView 类似,都是可以显示同一种类型 View 的集合的控件。
首先看看最简单的用法,四步走:
①接入 build.gradle 文件中加入
compile 'com.android.support:recyclerview-v7:24.0.0'
②创建对象
RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);
③设置显示规则
recyclerview.setLayoutManager(
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
RecyclerView 将所有的显示规则交给一个叫 LayoutManager 的类去完成了。
LayoutManager 是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是 LinearLayoutManager、 GridLayoutManager 、 StaggeredGridLayoutManager,从名字我们就能看出来了,分别是,线性显示、网格显示、瀑布流显示。当然你也可以通过继承这些类来扩展实现自己的 LayougManager。
④ 设置适配器
recyclerview.setAdapter(adapter);
适配器,同 ListView 一样,用来设置每个item显示内容的。
通常,我们写 ListView 适配器,都是首先继承 BaseAdapter,实现四个抽象方法,创建一个静态ViewHolder , getView() 方法中判断 convertView 是否为空,创建还是获取 viewholder 对象。
而 RecyclerView 也是类似的步骤,首先继承RecyclerView.Adapter<VH>
类,实现三个抽象方法,创建一个静态的 ViewHolder。不过 RecyclerView 的 ViewHolder 创建稍微有些限制,类名就是上面继承的时候泛型中声明的类名(好像反了,应该是上面泛型中的类名应该是这个holder的类名);并且 ViewHolder 必须继承自RecyclerView.ViewHolder
类。
1 public class DemoAdapter extends RecyclerView.Adapter<DemoAdapter.VH> { 2 private List<Data> dataList; 3 private Context context; 4 5 public DemoAdapter(Context context, ArrayList<Data> datas) { 6 this.dataList = datas; 7 this.context = context; 8 } 9 10 @Override 11 public VH onCreateViewHolder(ViewGroup parent, int viewType) { 12 return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null)); 13 } 14 15 @Override 16 public void onBindViewHolder(VH holder, int position) { 17 holder.mTextView.setText(dataList.get(position).getNum()); 18 } 19 20 @Override 21 public int getItemCount() { 22 return dataList.size(); 23 } 24 25 public static class VH extends RecyclerView.ViewHolder { 26 TextView mTextView; 27 public VH(View itemView) { 28 super(itemView); 29 mTextView = (TextView) itemView.findViewById(android.R.id.text1); 30 } 31 }
一些不常用的方法:
- 瀑布流与滚动方向
前面已经介绍过,RecyclerView
实现瀑布流,可以通过一句话设置:recycler.setLayoutManager(new StaggeredGridLayoutManager(2, VERTICAL))
就可以了。
其中 StaggeredGridLayoutManager 第一个参数表示列数,就好像 GridView 的列数一样,第二个参数表示方向,可以很方便的实现横向滚动或者纵向滚动。
使用 demo 可以查看:Github 【RecyclerView简单使用】
- 添加删除 item 的动画
同 ListView 每次修改了数据源后,都要调用 notifyDataSetChanged()
刷新每项 item 类似,只不过RecyclerView 还支持局部刷 新 notifyItemInserted(index);
、 notifyItemRemoved(position)
、notifyItemChanged(position)
。
在添加或删除了数据后,RecyclerView 还提供了一个默认的动画效果,来改变显示。同时,你也可以定制自己的动画效果:模仿 DefaultItemAnimator 或直接继承这个类,实现自己的动画效果,并调用recyclerview.setItemAnimator(new DefaultItemAnimator());
设置上自己的动画。
使用 demo 可以查看:Github 【RecyclerView默认动画】
- LayoutManager的常用方法
findFirstVisibleItemPosition()
返回当前第一个可见 Item 的 position findFirstCompletelyVisibleItemPosition()
返回当前第一个完全可见 Item 的 position findLastVisibleItemPosition()
返回当前最后一个可见 Item 的 position findLastCompletelyVisibleItemPosition()
返回当前最后一个完全可见 Item 的 position. scrollBy()
滚动到某个位置。
- adapter封装
其实很早之前写过一篇关于 RecyclerView 适配器的封装,所以这不再赘述了,传送门:RecyclerView的通用适配器
使用 demo 可以查看:Github 【RecyclerView通用适配器演示】
二.工作原理与ListView比较
类名 | 作用 |
---|---|
RecyclerView.LayoutManager | 负责Item视图的布局的显示管理 |
RecyclerView.ItemDecoration | 给每一项Item视图添加子View,例如可以进行画分隔线之类 |
RecyclerView.ItemAnimator | 负责处理数据添加或者删除时候的动画效果 |
RecyclerView.Adapter | 为每一项Item创建视图 |
RecyclerView.ViewHolder | 承载Item视图的子布局 |
1、LayoutManager工作原理
java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.support.v7.widget.RecyclerView
首先是 RecyclerView 继承关系,可以看到,与 ListView 不同,他是一个 ViewGroup。既然是一个 View,那么就不可少的要经历 onMeasure()
、onLayout()
、onDraw()
这三个方法。 实际上,RecyclerView 就是将onMeasure()
、onLayout()
交给了 LayoutManager 去处理,因此如果给 RecyclerView 设置不同的 LayoutManager 就可以达到不同的显示效果,因为onMeasure()
、onLayout()
都不同了嘛。
2、ItemDecoration 工作原理
ItemDecoration 是为了显示每个 item 之间分隔样式的。它的本质实际上就是一个 Drawable。当 RecyclerView 执行到 onDraw()
方法的时候,就会调用到他的 onDraw()
,这时,如果你重写了这个方法,就相当于是直接在 RecyclerView 上画了一个 Drawable 表现的东西。 而最后,在他的内部还有一个叫getItemOffsets()
的方法,从字面就可以理解,他是用来偏移每个 item 视图的。当我们在每个 item 视图之间强行插入绘画了一段 Drawable,那么如果再照着原本的逻辑去绘 item 视图,就会覆盖掉 Decoration 了,所以需要getItemOffsets()
这个方法,让每个 item 往后面偏移一点,不要覆盖到之前画上的分隔样式了。
3、ItemAnimator
每一个 item 在特定情况下都会执行的动画。说是特定情况,其实就是在视图发生改变,我们手动调用notifyxxxx()
的时候。通常这个时候我们会要传一个下标,那么从这个标记开始一直到结束,所有 item 视图都会被执行一次这个动画。
4、Adapter工作原理
首先是适配器,适配器的作用都是类似的,用于提供每个 item 视图,并返回给 RecyclerView 作为其子布局添加到内部。
但是,与 ListView 不同的是,ListView 的适配器是直接返回一个 View,将这个 View 加入到 ListView 内部。而 RecyclerView 是返回一个 ViewHolder 并且不是直接将这个 holder 加入到视图内部,而是加入到一个缓存区域,在视图需要的时候去缓存区域找到 holder 再间接的找到 holder 包裹的 View。
5、ViewHolder
每个 ViewHolder 的内部是一个 View,并且 ViewHolder 必须继承自RecyclerView.ViewHolder
类。 这主要是因为 RecyclerView 内部的缓存结构并不是像 ListView 那样去缓存一个 View,而是直接缓存一个 ViewHolder ,在 ViewHolder 的内部又持有了一个 View。既然是缓存一个 ViewHolder,那么当然就必须所有的 ViewHolder 都继承同一个类才能做到了。
6、缓存与复用的原理
RecyclerView 的内部维护了一个二级缓存,滑出界面的 ViewHolder 会暂时放到 cache 结构中,而从 cache 结构中移除的 ViewHolder,则会放到一个叫做 RecycledViewPool 的循环缓存池中。
顺带一说,RecycledView 的性能并不比 ListView 要好多少,它最大的优势在于其扩展性。但是有一点,在 RecycledView 内部的这个第二级缓存池 RecycledViewPool 是可以被多个 RecyclerView 共用的,这一点比起直接缓存 View 的 ListView 就要高明了很多,但也正是因为需要被多个 RecyclerView 公用,所以我们的 ViewHolder 必须继承自同一个基类(即RecyclerView.ViewHolder)。
默认的情况下,cache 缓存 2 个 holder,RecycledViewPool 缓存 5 个 holder。对于二级缓存池中的 holder 对象,会根据 viewType 进行分类,不同类型的 viewType 之间互不影响。
三 .源码解析
1、onMeasure
既然是一个 View,我们先从onMeasure()
开始看。
之前我们就说了 RecyclerView 的 measure 和 layout 都是交给了 LayoutManager 去做的,来看一下为什么:
1 if (mLayout.mAutoMeasure) { 2 final int widthMode = MeasureSpec.getMode(widthSpec); 3 final int heightMode = MeasureSpec.getMode(heightSpec); 4 final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY 5 && heightMode == MeasureSpec.EXACTLY; 6 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); 7 } else { 8 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); 9 }
不论是否启用 mAutoMeasure 最终都会执行到 mLayout.onMeasure() 方法中,而这个 mLayout 就是一个 LayoutManager 对象。
我们挑选 LinearLayoutManager 来看
发现它并没有onMeasure()
方法,LinearLayoutManager 直接继承自 LayoutManager,所以又回到了父类 LayoutManager 中。
1 void defaultOnMeasure(int widthSpec, int heightSpec) { 2 // calling LayoutManager here is not pretty but that API is already public and it is better 3 // than creating another method since this is internal. 4 final int width = LayoutManager.chooseSize(widthSpec, 5 getPaddingLeft() + getPaddingRight(), 6 ViewCompat.getMinimumWidth(this)); 7 final int height = LayoutManager.chooseSize(heightSpec, 8 getPaddingTop() + getPaddingBottom(), 9 ViewCompat.getMinimumHeight(this)); 10 11 setMeasuredDimension(width, height); 12 }
有一句非常奇葩的注释:在这里直接调用 LayoutManager 静态方法并不完美,因为本身就是在类内部,更好的办法调用一个单独的方法。但反正这段代码也已经公开了,你们自己看着办。。。。。。
如果这不是历史遗留问题,那肯定是临时工写的,你写的时候都意识到这问题了,你还把一大堆类都写在一个类里面,造成了 RecyclerView 一个类有一万多行代码。我猜你是为了类之间跨类调用方便一点,可是你就不能设置一个包访问权限,所有类成员方法都包内调用吗,一个类干了六个类的活,网上居然还有人说这是高内聚的表现。
接着是chooseSize()
方法,很简单,直接根据测量值和模式返回了最适大小。
1 public static int chooseSize(int spec, int desired, int min) { 2 final int mode = View.MeasureSpec.getMode(spec); 3 final int size = View.MeasureSpec.getSize(spec); 4 switch (mode) { 5 case View.MeasureSpec.EXACTLY: 6 return size; 7 case View.MeasureSpec.AT_MOST: 8 return Math.min(size, Math.max(desired, min)); 9 case View.MeasureSpec.UNSPECIFIED: 10 default: 11 return Math.max(desired, min); 12 } 13 }
紧接着是对子控件 measure ,调用了:dispatchLayoutStep2()
调用了相同的方法,子控件的 measure 在 layout 过程中讲解
2、onLayout
然后我们来看 layout 过程. 在onLayout()
方法中间接的调用到了这么一个方法:dispatchLayoutStep2()
,在它之中又调用到了mLayout.onLayoutChildren(mRecycler, mState);
我们重点看这个onLayoutChildren()
方法。
这个方法在 LayoutManager 中的实现是空的,那么想必是在子类中实现了吧。还是找LinearLayoutManager ,跟上面 measure 过程一样,调用了dispatchLayoutStep2()
跟进去发现这么一个方法:
fill(recycler, mLayoutState, state, false);
onLayoutChildren() 中有一个非常重要的方法:fill()
recycler,是一个全局的回收复用池,用于对每个itemview回收以及复用提供支持。稍后会详细讲这个。
1 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { 2 layoutChunk(recycler, state, layoutState, layoutChunkResult); 3 layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; 4 5 if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) { 6 layoutState.mScrollingOffset += layoutChunkResult.mConsumed; 7 if (layoutState.mAvailable < 0) { 8 layoutState.mScrollingOffset += layoutState.mAvailable; 9 } 10 recycleByLayoutState(recycler, layoutState); 11 } 12 }
fill() 作用就是根据当前状态决定是应该从缓存池中取 itemview 填充 还是应该回收当前的 itemview。
其中,layoutChunk() 负责从缓存池 recycler 中取 itemview,并调用View.addView()
将获取到的 ItemView 添加到 RecyclerView 中去,并调用 itemview 自身的 layout 方法去布局 item 位置。
同时在这里,还调用了measureChildWithMargins()
来测绘子控件大小以及设置显示位置。这一步,我们到下面的 draw 过程还要讲。
而这全部的添加逻辑都放在一个 while 循环里面,不停的添加 itemview 到 recyclerview 里面,直到塞满所有可见区域为止。
3、onDraw
1 @Override 2 public void onDraw(Canvas c) { 3 super.onDraw(c); 4 final int count = mItemDecorations.size(); 5 for (int i = 0; i < count; i++) { 6 mItemDecorations.get(i).onDraw(c, this, mState); 7 } 8 }
在 onDraw()
中,除了绘制自己以外,还多调了一个mItemDecorations 的 onDraw() 方法,这个mItemDecorations 就是前面吐槽的分隔线的集合。
之前在讲 RecyclerView 的五虎上将的时候就讲过这个 ItemDecoration。 当时我们还重写了一个方法叫getItemOffsets()
目的是为了不让 itemview 挡住分隔线。那他是在哪调用的呢?
还记得 layout 时说的那个measureChildWithMargins()
吗,就是在这里:
1 public void measureChildWithMargins(View child, int widthUsed, int heightUsed) { 2 final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); 3 widthUsed += insets.left + insets.right; 4 heightUsed += insets.top + insets.bottom; 5 6 if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { 7 child.measure(widthSpec, heightSpec); 8 } 9 }
在 itemview measure 的时候,会把偏移量也计算进来,也就是说:其实 ItemDecoration 的宽高是计算在 itemview 中的,只不过 itemview 本身绘制区域没有那么大,留出来的地方正好的透明的,于是就透过 itemview 显示出了 ItemDecoration。那么就很有意思了,如果我故意在 ItemDecoration 的偏移量中写成0,那么 itemview 就会挡住 ItemDecoration,而在 itemview 的增加或删除的时候,会短暂的消失(透明),这时候就又可以透过 itemview 看到 ItemDecoration 的样子。使用这种组合还可以做出意想不到的动画效果。