RecyclerView
RecyclerView,顾名思义,这个View代表的就是一个可循环使用的视图集合控件,它定义了ViewHolder类型标准,封装了View缓存判断逻辑,更强大的是它可以通过一个LayoutManager将一个RecyclerView显示为不同的样式。
目的: 在有限的屏幕内展示大量的内容。
RecyclerView的五虎将
RecyclerView.LayoutManager :负责Item视图的布局的显示管理
RecyclerView.ItemDecoration:给每一项Item视图添加子View,例如可以进行画分割线之类的
RecyclerView.ItemAnimator:负责处理数据添加或者删除时候的动画效果
RecyclerView.Adapter:为每一项Item创建视图
RecyclerView.ViewHolder:承载Item视图的子布局
常用方法
RecyclerView与ListView、GridView类似,都是可以显示同一类型View的集合的控件。首先看看最简单的用法,四步走:
0.介入build.gradle文件中加入
compile 'com.android.support:recyclerview-v7:24.0.0'
1.创建对象
RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);
2.设置RV的布局管理器,决定了RV的显示风格
recyclerview.setLayoutManager(new LinearLayoutManager(this,
LinearLayoutManager.VERTICAL, false));
RecyclerView将所有的显示规则交给LayoutManager去完成,LayoutManager是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是LinearLayoutManager、GridLayoutManager、StaggerdGridlayoutManager,从名字我们就可以看出,分别是,线性显示,网格显示,瀑布流显示,当然你也可以通过继承这些类来扩展自己的LayoutManager。
3.设置适配器
recyclerview.setAdapter(adapter);
适配器,同ListView一样,用来设置每个item显示内容。通常,我们写ListView适配器,都是首先继承BaseAdapter,实现四个抽象方法(getView, getItem, getCount, getItemId),创建一个静态ViewHolder,getView()方法中判断convertView是否为空,创建还是获取viewHolder对象。
而RecyclerView也是类似的步骤,首先继承RecyclerView.Adapter类,实现三个抽象方法,创建一个静态类的ViewHolder,不过RecyclerView的ViewHolder创建稍微有些限制,类名就是上面继承的时候泛型中声明的类名,并且ViewHOder必须继承自RecyclerView.ViewHolder类
public class DemoAdapter extends RecyclerView.Adapter<DemoAdapter.VH> {
private List<Data> dataList;
private Context context;
public DemoAdapter(Context context, ArrayList<Data> datas) {
this.dataList = datas;
this.context = context;
}
@Override
public VH onCreateViewHolder(ViewGroup parent, int viewType) {
return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
}
@Override
public void onBindViewHolder(VH holder, int position) {
holder.mTextView.setText(dataList.get(position).getNum());
}
@Override
public int getItemCount() {
return dataList.size();
}
public static class VH extends RecyclerView.ViewHolder {
TextView mTextView;
public VH(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(android.R.id.text1);
}
}
}
其他方法
除了常用的方法,还有不常用的方法。
瀑布流与滚动方向
前面已经介绍过了,RecyclerView实现瀑布流,可以通过一句话设置:RecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,VERTICAL))就可以了。
其中StaggerGridLayoutManager的第一个参数表示列数,就好像GridView的列数一样,第二个参数表示方向,可以很方便的实现横向滚动或者纵向滚动。
添加删除item的动画
同ListView每次修改了数据后,都要调用notifyDataSetChanged刷新每项item类似,只不过RecyclerView还支持局部刷新notifyItemInserted(index);notifyItemRemoved(position);notifyItemChanged(position);
在添加或删除了数据时,recyclerView还提供了一个默认的动画效果,来改变显示,同时你也可以定制自己的动画效果:模仿DefaultItemAnimator或者直接继承这个类,实现自己的动画效果。并调用RecyclerView.setItemAnimator(new DefaultItemAnimator())设置上自己的动画。
LayoutManager的常用方法
findFistVisibleItemPosition();返回当前第一个可见Item的position
findFirstCompletelyVisibleItemPosition();返回当前第一个完全可见的Item的position
findLastVisibleItemPosition();返回当前最后一个可见Item的position
findComplete领了也VisivleItemPosition();返回当前最后一个完全可见Item的Position
ScrollBy();滚动到某个位置
LayoutManager工作原理
首先RecyclerView继承关系,可以看到与ListView不同,他是一个ViewGroup,既然是一个View,那么不可少的就是经历onMeasure()、onLayout()、onDraw()这三个方法,实际上RecyclerView就是将onMeasure()、onLayout()交给了LayoutManager去处理,因此如果给RecyclerView设置不同的LayoutManager就可以达到不同的显示效果,因为onMeasure()、onLayout()都不同。
ItemDecoration 工作原理
ItemDecoration是为了显示每个Item之间分隔样式的,它的本质上就是一个Drawable。当RecyclerView执行到onDraw()方法的时候,就会调用它的onDraw(),这时,如果你重写了这个方法,就相当于直接在RecyclerView上画了一个Drawable表现的东西,而最后,在它的内部还有一个叫getItemOffsets()的方法,从字面就可以理解,它是用来偏移每个Item视图的,当我们在每个Item视图之间强行插入一段Drawable,那么如果在照着原本的逻辑去给item视图,就会覆盖掉Decoration了,所以需要getItemOffsets()这个方法,让每个item往后偏移一点,不要覆盖到之前画的上的分个样式。
ItemAnimatior
每一个item在特定情况下都会执行的动画,说是特定情况,其实就是在视图发生改变,我们手动调用notifyxxxx()的时候,通常这个时候我们要传一个下标,那么从这个标记开始一直到结束,所有item都会被执行一次这个动画
Adapter工作原理
首先是适配器,适配器的作用都是类似的,用于提供每一个item,并返回给RecyclerView作为其子布局添加到内部,但是,与ListView不同的是:ListView的适配器是直接返回一个View,将这个View加入到ListView内部;RecyclerView是返回一个ViewHolder并且不是直接将这个holder加入到视图内部,而是加入一个缓存区域,在视图需要的时候去缓存区找到holder在间接找到holder包裹的View
ViewHolder
每一个ViewHolder的内部都是一个View,并且ViewHolder必须继承自RecyclerView.ViewHolder类,这主要是因为RecyclerView内部的缓存结构并不是像ListView那样去缓存一个View,而是直接缓存一个ViewHolder,在ViewHolder的内部又持有一个View,既然是缓存一个ViewHolder,那么当然所有ViewHolder都继承同一个类才能做到。
缓存与复用的原理
RecyclerView的内部维护了一个四级缓存,滑出界面的ViewHolder会暂时放到cache结构中,而从cache结构中移除的ViewHolder,则会放到一个叫做RecyclerViewPool的循环缓存池中。
顺带一说,RecyclerView的性能并不比ListView好多少,它的最大的优势在于其扩展性,但是有一点,在RecyclerView内部的这个第二级缓存池RecyclerViewPool是可以被多个RecyclerView公用的。这一点比起直接缓存View的ListView就要高明了很多,但是正是应为需要被多个RecyclerView公用,所以我们的ViewHolder必须继承自同一个基类。
默认的情况下,cache缓存2个holder,RecyclerViewPool缓存5个holder,对于二级缓存池中holder对象,会根据ViewType进行分类,不同类型的ViewType之间互不影响。缓存这块后面有具体分析。
源码解析
onMeasure
既然是一个自定义View,那么我们先从onMeasure()开始看
之前我们说RecyclerView的Measure和layout都交给了LayoutManager去做
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 如果RV是 精确模式,则直接使用 mLayout测量宽高即可
if(skipMeasure || mAdapter == null) return;
} else {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
}
不论是否启用mAutoMeasure最终都会调用mlayout.onMeasure()方法中,而这个mLayout就是一个LayoutManager对象
我们先挑LinearLayoutManager来看,发现并没有它的onMeasure方法,LinearLayoutManager直接继承自LayoutManager,所以又回到了父类LayoutManager中。
void defaultOnMeasure(int widthSpec, int heightSpec) {
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
有一句非常奇葩的注释:在这里直接调用LayoutManager静态方法并不完美,因为本身就是在内部类,更好的办法调用一个单独的方法
接着是chooseSize()方法,很简单,直接根据测量值和模式返回了最适大小
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
紧接着是对子控件Measure,调用了dispatchLayoutStep2(),调用了相同的方法,子控件的Measure在layout过程中讲解
onLayout
然后我们看layout过程,在onLayout()方法中间接调用到这么一个方法:dispatchLayoutStep2(),在它之中又调用到了mLaoutChildern()方法
这个方法在LayoutManager中的实现是空的,那么想必是在子类中实现的,然后找到LinearLayoutManager,根上面Measure过程一样,调用dispatchLayoutStep2()跟进去有这么一个方法:
fill(recycler, mLayoutState, state, false);
recycler,是一个全局的回收复用池,用于对每个itemView回收及其复用提供支持,
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore (state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
viod layoutChunk() {
View view = layoutState.next(recycler);
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) { // 使用缓存的view,并添加进RV中
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
measureChildWithMargins(view, 0, 0); // 测量被添加到 RV 中的 item 的宽高
// To calculate correct layout position, we subtract margins.
// 根据所设置的 Decoration,Margins等属性确定 item 的显示位置
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
fill()作用就是根据当前状态LayoutState决定是应该从缓存池中取itemview填充,还是应该回收当前的itemview,完成子view 的测量布局操作。使用 while 循环判断是否有足够的空间来绘制一个完整的子view
其中layoutChunk()负责从缓存池recycler中取itemview,并调用addView()将取到的itemview添加到RecyclerView中去,并调用itemview自身的layout方法区布局item的位置
同时在这里,还调用MeasureChildWithMargins()来测绘子控件带下以及设置显示位置,这一步我们在下面的draw过程中讲。
而这全部的添加逻辑都放在一个while循环里面,不停的添加itemview到RecyclerView里面,直到塞满所有可见区域为止.
onDraw
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
在onDraw中除了绘制自己以外,还多调用了一个mItemDecorations的onDraw方法,这个mItemDecorations就是前面提到的分割线的集合
之前在讲RecyclerView的五虎上将的时候就讲过这个ItemDecoration,当时我们还重写了一个方法叫getItemOffsets() 目的是为了不让itemview挡住分割线。
还记得layout时说的那个MeasureChildWithMargins(),就在这里
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
在itemview Measure的时候,会把偏移量也计算出来,也就是说:其实ItemDecoration的宽高是计算在itemview中的,只不过itemview本身绘制区域没有那么大,留出来的地方正好的透明的,于是就透过itemview显示出来,ItemDecoration,那么就很有意思了,如果我故意在ItemDecoration的偏移量中写成0,那么itemview就会挡住ItemDecoration,而在itemview的增加或删除的时候,会短暂的消失,这时候又可以透过itemview看到ItemDecoration的样子,使用这种组合还可以做出意想不到的动画效果。
小结 RV 将 测量和布局的工作放心的委托给了 LayoutManager 来执行。不同的布局风格使用不同的 LayoutManager ,这是一种策略模式。
滚动
前面我们已经完整的走完了RecyclerView的绘制流程,接下来我们再看看它在滚动的时候代码又是怎么调用的,自然要看onTouch()方法的MOVE状态
case MotionEvent.ACTION_MOVE: {
final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
...
}
if (mScrollState != SCROLL_STATE_DRAGGING) {
...
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;
看到这个代码的时候,我们就会有疑问MotionEventCompa这个类是干什么的,它是V4包里面提供的一个工具类,用于兼容低版本的触摸屏手势,平时用的时候更多的是用它来处理多点触控的情况,当成MotionEvent就可以了。
dispatchNestedPreScroll(),用于处理嵌套逻辑,例如在ScrollView里面放一个RecyclerView,如果是以前用ListView,还得把高度写死,禁止ListView的复用和滚动逻辑,而RecyclerView则完全不需要更多处理,直接用就是了,而且有一个非常好的地方,如果放到ScrollView里面,ListView的Itemview是不会复用的,如果RecyclerView因为是全局公用一套缓存池,虽说嵌套到ScrollView效率会低很多,但是比起ListView嵌套要好很多,之后将缓存池的时候我们继续讲,
再之后,如果在相应方向上手指move的距离达到最大值,则认为需要滚动,并且设置为滚动状态。
接着走出if块,如果是滚动状态,则调用滚动方法scrollByInternal()执行相应方向的滚动,滚动的距离当然就是手指移动的距离,跟进去,它就是调用了LinearLayoutManager.scrollBy()方法
回收与复用
前面讲layout、滚动的时候,都出现了一个东西,叫Recycler。
public final class Recycler {
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
private ArrayList<ViewHolder> mChangedScrap = null;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
private RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
Recycler 的缓存复用机制就是通过Recycler 中的这些数据容器来实现的。它根据访问优先级从上到下可以分为
4级。
mAttachedScrap&mChangedScrap
mCachedViews
ViewCacheExtension
RecycledViewPoll
第一级缓存 mAttachedScrap&mChangedScrap
主要用来缓存屏幕内的ViewHolder。为什么要缓存屏幕内的VH那?
当我们通过下拉刷新来更新列表内容时,不会创建新的VH,只是在原来的VH 基础上进行重新绑定新的数据即可,而这些旧的VH就被保存在这两个集合中。即当我们调用 notifyXXX 方法时,就会向这两个列表进行填充,将旧VH缓存起来。
第二级缓存:mCachedViews
就是上面的一系列mCachedViews,如果仍依赖于RecyclerView(比如已经滑出可视范围,但还没有被移除掉),但已经被标记移除的ItemView集合会被添加到mAttachScrap中,然后如果mAttachedScrap中不在依赖时会被加入到mCachedViews中,mChangedScrap则是存储notifyxxxx方法时需要改变ViewHolder
用来缓存移除至屏幕外的VH,默认情况下缓存个数是2个,可以通过 setViewCachedSize 来改变缓存的容量大小。如果mCachedViews已满,则会根据FIFO 移除旧VH添加新VH。
试想一下,刚被移除屏幕的VH 有可能接下来马上就会再次使用,所以RV不会立即将其设置为无效的VH,而是将他们保存到 cache 中。出于内存消耗的问题,cache 不能将所有移除屏幕外的VH 都缓存起来,所以它的默认容量是2.
第三级缓存 ViewCacheExtension
ViewCacheExtension是一个抽象静态类,用于充当附加的缓存池,当RecyclerView从第一级缓存找不到需要的View时,将会从ViewCacheExtension中找,不过这个缓存是由开发者维护的,如果没有设置它,则不会启用,通常我们也不会去设置它,系统已经预先提供了两级缓存,除非有特殊需求,比如在调用系统的缓存池之前,返回一个特定的视图,才会用到它。
第四级缓存 RecycledViewPool
同样是用来缓存屏幕外的ViewHolder. 当 mCachedViews 中的个数已满,则从mCachedViews 中淘汰的 VH会缓存到RecycledViewPool中。RecycledViewPool 会将 VH 的内部数据全部清理,因此从RecycledViewPool中取出来的 VH 需要重新调用 onBindViewHolder 绑定数据
之前讲了,与ListView直接缓存Itemview不同,从上面代码中我们也能看到,RecyclerView缓存的是ViewHolder,而ViewHolder里面包含了一个View这也就是为什么写Adapter的时候,必须继承一个固定的ViewHolder的原因,我们来看一下RecyclerViewPool
public static class RecycledViewPool {
// 根据 viewType 保存的被废弃的 ViewHolder 集合,以便下次使用
private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();
/**
* 从缓存池移除并返回一个 ViewHolder
*/
public ViewHolder getRecycledView(int viewType) {
final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
if (scrapHeap != null && !scrapHeap.isEmpty()) {
final int index = scrapHeap.size() - 1;
final ViewHolder scrap = scrapHeap.get(index);
scrapHeap.remove(index);
return scrap;
}
return null;
}
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
/**
* 根据 viewType 获取对应缓存池
*/
private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
ArrayList<ViewHolder> scrap = mScrap.get(viewType);
if (scrap == null) {
scrap = new ArrayList<>();
mScrap.put(viewType, scrap);
if (mMaxScrap.indexOfKey(viewType) < 0) {
mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
}
}
return scrap;
}
}
从名字看来,他是一个缓存池,实现上,是通过一个默认大小为5的ArrayList实现的,这一点,同ListView的RecyclerBin这个类一样,很奇怪为什么不用LinkedList来做,按理说这种不需要索引读取的缓存池,用链表最合适的.
LinkedList 使用了双向链表,在内存占用上时ArrayList的两倍。虽然,该缓存池的删除添加操作比较频繁,而LinkedList在这方便比较有优势,但是ArrayList的容量比较小,性能上消耗也不是太大。总的来说,这是一种比较折中的设计方案。
然后每一个ArrayList又都是放在一个Map里面,SparseArray这个类我们在讲性能优化的时候已经多次提到了,就是用两个数组,用来代替HashMap
把所有的ArrayList放在一个Map里面,这也是RecyclerView最大的亮点,这样根据itemType来取不同的缓存Holder,每一个Holder都有对应的缓存,而只需要为这些不同RecyclerView设置同一个Pool就可以了。
为什么RecyclerView比ListView好
在适配器中通过onBindViewHolder和onCreateViewHolder两个方法屏蔽了一些固定的逻辑,使得适配器使用更简单,又通过了LayoutManager实现具体的布局,使得RecyclerView具有更强大的定制能力,所以RecyclerView比ListView好。