个人回顾:
1.RecyclerView用来代替ListView和GridView
2.RecyclerView可以实现瀑布流布局
3.RecyclerView最出色的是它的缓存机制(四级缓存),ListView只有2级缓存
1.RecyclerView
RecyclerView是Android 5.0推出的,是support-v7包中的新组件,用来代替ListView和GridView,并且能够实现瀑布流的布局,更加高级、灵活,而且提供了更高效的回收复用机制,同时实现了管理与视图的解耦合。RecyclerView提供了一种插拔式的体验,高度的解耦,异常的灵活,只需设置不同的LayoutManager、ItemAnimator和ItemDecoration,就能实现不同的效果。
官方的介绍:
A flexible view for providing a limited window into a large data set.
就一句话:为大量数据集提供一个有限的展示窗口的灵活视图。
RecyclerView的优点:
①支持局部刷新。
②可以自定义item增删时的动画。
③能够实现item拖拽和侧滑删除等功能。
④默认已实现View的复用,而且回收机制更加完善。
RecyclerView的用法:
①在build.gradle文件中添加依赖
compile ‘com.android.support:recyclerview-v7:26.1.0’
②主页面布局添加RecyclerView控件
android:layout_width=“match_parent”
android:layout_height=“match_parent” />
③item布局,为RecyclerView内的元素设定布局样式
< LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:orientation=“horizontal”>
< TextView
android:id="@+id/tv_content"
android:layout_width=“match_parent”
android:layout_height=“50dp”
android:gravity=“center”
android:text=“数据” />
< /LinearLayout>
④创建适配器,继承RecyclerView.Adapter
(1)创建适配器类,继承自RecyclerView.Adapter,泛型传入RecyclerView.ViewHolder类。
(2)创建内部类即RecyclerView.ViewHolder类的子类,并初始化item的控件。
(3)重写RecyclerView.Adapter类的相关方法。
public class MyRecycleViewAdapter extends RecyclerView.Adapter
private List mList;//数据源
MyRecycleViewAdapter(List list) {
mList = list;
}
//创建ViewHolder并返回,后续item布局里控件都是从ViewHolder中取出
@Override
public MyHolder onCreateViewHolder( ViewGroup parent, int viewType) {
View view = LayoutInflater.from( parent.getContext()).inflate(R.layout.item_one, parent, false);
MyHolder holder = new MyHolder(view);
return holder;
}
//将数据绑定到ViewHolder中
@Override
public void onBindViewHolder(MyHolder holder, int position) {
holder.textView.setText( mList.get(position).toString());
}
//获取数据源总的条数
@Override
public int getItemCount() {
return mList.size();
}
//自定义内部类ViewHolder,绑定控件
class MyHolder extends RecyclerView.ViewHolder {
TextView textView;
public MyHolder(View itemView) {
super(itemView);
textView = itemView.findViewById( R.id.tv_content);
}
}
}
⑤在MainActivity中使用RecyclerView
(1)获取RecyclerView对象 。
(2)为RecyclerView设置LayoutManager。
(3)为RecyclerView设置Adapter 。
public class MainActivity extends Activity {
private RecyclerView mRecycleView;
private MyRecycleViewAdapter mAdapter;
private LinearLayoutManager mLinearLayoutManager;//布局管理器
private List mList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mList = new ArrayList();
mRecycleView = findViewById(R.id.rv_list);
initData(mList);
//创建布局管理器(可水平或竖直)
mLinearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mAdapter = new MyRecycleViewAdapter( mList);
mRecycleView.setLayoutManager( mLinearLayoutManager);//设置布局管理器
mRecycleView.setAdapter(mAdapter);
}
public void initData(List list) {
for (int i = 1; i <= 40; i++) {
list.add(“第” + i + “条数据”);
}
}
}
使用方法很简单,注意几点:
①Adapter
使用时需要创建adapter类,该类继承于RecyclerView.Adapter< VH>,其中VH是adapter类中创建的一个继承于RecyclerView.ViewHolder的静态内部类。
adapter类主要有3个方法和1个自定义ViewHolder:
(1)onCreateViewHolder: 创建ViewHolder并返回,后续item布局里控件都是从ViewHolder中取出。
(2)onBindViewHolder:通过方法提供的ViewHolder,将数据绑定到ViewHolder中。
(3)getItemCount:获取数据源总的条数。
(4)MyHolder :这是RecyclerView.ViewHolder的实现类,用于初始化item布局中的子控件。注意,在这个类的构造方法中需要传递item布局的View给父类 。
②LayoutManager
布局管理器,通过不同的布局管理器来控制item的排列顺序,负责item元素的布局和复用。RecycleView提供了三种布局管理器:
(1)LinearLayoutManager:线性布局,以垂直或水平滚动列表方式显示item。
(2)GridLayoutManager:网格布局,在网格中显示item。
(3)StaggeredGridLayoutManager:瀑布流布局,在分散对齐网格中显示item。
如遇到特殊需求,也可以通过继承RecyclerView.LayoutManager来自定义LayoutManager,重写它的方法来实现所需要的效果。
⑥事件监听
RecyclerView没有提供现成的点击事件监听,需要开发者自己去实现。实现方式很简单,可以在RecyclerView的Adapter中自定义一个接口,并创建一个供其他类设置监听的方法。
public class MyRecycleViewAdapter extends RecyclerView.Adapter
private List mList;//数据源
private OnItemClickListener onItemClickListener;
//供外部调用设置监听
public void setOnItemClickListener( OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
//自定义的接口
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
@Override
public void onBindViewHolder(final MyHolder holder, int position) {
holder.textView.setText( mList.get(position).toString());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(v, holder.getAdapterPosition() + 1);
}
}
});
}
}
当定义好接口后,在onBindViewHolder()方法中为holder.itemView(itemView是列表中的每一个item项)设置点击事件监听,然后在onClick()中判断如果有用户传递onItemClickListener实例进来,就调用它的onItemClick(),将点击事件转移到自定义接口上,传给外面的调用者。调用者代码如下:
mAdapter.setOnItemClickListener(new MyRecycleViewAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position){
Toast.makeText(getApplicationContext(), “第” + position + “条数据”, Toast.LENGTH_SHORT ).show();
}
});
⑦ItemAnimator 动画
RecyclerView可以通过setItemAnimator( ItemAnimator animator)来设置添加和移除时的动画效果。ItemAnimator是一个抽象类,RecyclerView提供了一个它的实现类,即DefaultItemAnimator。
//设置动画效果
mRecycleView.setItemAnimator(new DefaultItemAnimator());
在adapter中添加两个方法,用于添加和移除Item。注意,更新数据集要用notifyItemInserted(position)与notifyItemRemoved(position) ,而不是notifyDataSetChanged(),否则没有动画效果。
// 添加数据
public void addItem() {
mList.add(0, "new ");
notifyItemInserted(0);
}
//移除数据
public void removeItem(int position) {
mList.remove(position);
notifyItemRemoved(position);
}
按下底部“添加”按钮会在顶部插入数据,点击列表中的Item则删除该条数据。
如果对这种动画效果不满意,也可以去自定义各种动画效果。目前github上有许多开源的项目,例如RecyclerViewItemAnimators,可以直接去引用或学习它的动画效果。
2.RecyclerView的缓存机制——进阶
RecyclerView在有大量数据时依然可以顺畅的滑动,这得益于它优秀的缓存机制。
RecyclerView本身是一个ViewGroup,因此在滑动时就避免不了添加或移除子View(子View通过RecyclerView#Adapter中的onCreateViewHolder创建),如果每次使用子View都要重新创建,肯定会影响滑动的流畅性,所以RecyclerView通过Recycler来缓存ViewHolder(内部包含子View),这样在滑动时可以复用子View,某些条件下还可以复用子View绑定的数据。所以本质上来说,RecyclerView之所以能够实现顺畅的滑动效果,是因为缓存机制,因为缓存减少了重复绘制View和绑定数据的时间,从而提高了滑动时的性能。
在学习Recycler的缓存结构之前,先了解两个词汇:
①Scrap (view):在布局期间进入临时分离状态的子视图。废弃视图可以重复使用,而不会与父级RecyclerView完全分离,如果不需要重新绑定,则不进行修改,如果视图被视为脏,则由适配器修改。(这里的脏是指那些在展示之前必须重新绑定的视图,比如一个视图原来展示的是“张三”,之后需要展示“李四”了,那么这个视图就是脏视图,需要重新绑定数据后再展示的。)
②Recycle (view):先前用于显示适配器特定位置的数据的视图可以放置在高速缓存中供稍后重用再次显示相同类型的数据。这样可以跳过初始布局或构造,显著提高性能。
首先看一下RV的内部类Recycler。
public final class Recycler {
final ArrayList< ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList< ViewHolder> mChangedScrap = null;
final ArrayList< ViewHolder> mCachedViews = new ArrayList< ViewHolder>();
RecycledViewPool mRecyclerPool;
private ViewCacheExtension mViewCacheExtension;
……
}
这个类掌握着RV的缓存大权,从上面的代码可以看到这个类声明了五个成员变量:
①mAttachedScrap:这个变量是个存放ViewHolder对象的ArrayList,这一级缓存是没有容量限制的。前面讲两个专业术语的时候提到了Scrap,这个就属于Scrap中的一种,这里的数据是不做修改的,不会重新走Adapter的绑定方法。
②mChangedScrap:这个变量和上边的mAttachedScrap是一样的,唯一不同的是,它存放的是发生了变化的ViewHolder,如果使用到了这里缓存的ViewHolder需要重新走Adapter的绑定方法。
③mCachedViews:这个变量同样是一个存放ViewHolder对象的ArrayList,但是这个不同于上面的两个,里面存放的是dettach掉的视图,即已经remove掉的视图,已经和RV是分离关系的视图,但是它里面的ViewHolder依然保存着之前的信息,比如position和绑定的数据等等。这一级缓存是有容量限制的,默认是2。
④mRecyclerPool:这个变量本身是一个类,跟上面三个都不一样。这里面保存的ViewHolder不仅仅是removed掉的视图,而且是恢复了出厂设置的视图,任何绑定过的痕迹都没有了。想用这里缓存的ViewHolder,那一定要重新走Adapter的绑定方法了。而且RV是支持多布局的,所以这里的缓存是按照itemType来分开存储的,大致的看一下它的结构:
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
ArrayList< ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
……
}
SparseArray< ScrapData> mScrap = new SparseArray<>();
……
}
首先是一个常量DEFAULT_MAX_SCRAP,这个就是缓存池定义的默认的缓存数,这个缓存数可以自己修改。注意这个缓存数量不是指整个缓存池只能缓存这么多,而是每个不同itemType的ViewHolder的缓存数量。
接着往下看,是一个静态内部类ScrapData,这里只看跟缓存相关的两个变量,先说mMaxScrap,前面的常量赋值给了它,这也就印证了前面说的这个缓存数量是对应每一种类型的ViewHolder的。再来看这个mScrapHeap变量,同样是一个缓存ViewHolder对象的ArrayList,默认容量是5。
最后看到mScrap变量,它是一个存储ScrapData类的对象的SparseArray,这样RecyclerPool就把不同itemType的ViewHolder按类型分类缓存了起来。
⑤mViewCacheExtension:这一级缓存是留给开发者自由发挥的,官方并没有默认实现,它本身是null。
综上所述,RecyclerView的缓存分为四级,优先级从高到底依次为:
①一级缓存mAttachedScrap&mChangedScrap :
mAttachedScrap存储的是当前屏幕中的ViewHolder,它的数据结构是ArrayList。在调用LayoutManager的onLayoutChildren方法对views进行布局时,会将RecyclerView显示的Views全部暂存到该集合中。该缓存中的ViewHolder的特性是,如果和RV上的position或者itemId匹配上了就可以直接拿来使用的,无需调用onBindViewHolder方法重新绑定数据。
mChangedScrap和mAttachedScrap属于同一级别的缓存,不过mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。
②二级缓存mCachedViews:
mCachedViews缓存的是滑动时即将与RecyclerView分离的ViewHolder,按子View的position或id缓存,默认最多存放2个。mCachedViews对应的数据结构是ArrayList,但是该缓存对集合的大小是有限制的。该缓存中ViewHolder的特性和mAttachedScrap中的特性是一样的,只要position或者itemId对应就无需重新绑定数据。
开发者可以调用setItemViewCacheSize(size)方法来改变缓存的大小,该层级缓存触发的一个常见的场景是滑动RecyclerView。当然调用notify()也会触发该缓存。
③三级缓存ViewCacheExtension:
ViewCacheExtension是需要开发者自己实现的缓存,基本上页面上的所有数据都可以通过它进行实现。
④四级缓存RecycledViewPool :根据ViewType来缓存ViewHolder,每个ViewType的数组大小为5,可以动态的改变。该缓存的ViewHolder缓存池,本质上是一个SparseArray,其中key是ViewType(int类型),value存放的是 ArrayList< ViewHolder>,默认每个ArrayList中最多存放5个ViewHolder。
注意:
(1)mAttachedScrap存储的是当前还在屏幕中的ViewHolder。实际上是从屏幕上分离出来的ViewHolder,但是又即将添加到屏幕上去的ViewHolder。比如说,RecyclerView上下滑动,滑出一个新的Item,此时会重新调用LayoutManager的onLayoutChildren方法,从而会将屏幕上所有的ViewHolder先scrap掉(即废弃掉),添加到mAttachedScrap里面去,然后在重新布局每个ItemView时,会优先从mAttachedScrap里面获取,这样效率就会非常高。这个过程不会重新onBindViewHolder。
(2)mCachedViews默认大小为2,不过通常是3,3由默认的大小2 + 预取的个数1。所以在RecyclerView首次加载时,mCachedViews的size为3。通常来说,可以通过RecyclerView的setItemViewCacheSize方法设置大小,但是这个不包括预取大小;预取大小通过LayoutManager的setItemPrefetchEnabled方法来控制。
3.RecyclerView和ListView对比
ListView不强制实现ViewHolder,但是后来google建议实现ViewHolder模式。先分别看一下这两种不同的方式。
不使用ViewHolder的模式:
4.源码分析(各缓存的使用)
知道了RV的各缓存层级,接下来看看它们是怎么工作的、为什么要设计这些层级?从最简单的布局管理者LinearLayoutManager入手,来看看到底如何使用这几级缓存写出一个合格的布局管理者。
(1)RV从无到有的加载过程
一个视图的显示要经过onMeasure、onLayout、onDraw三个方法,就从第一个方法onMeasure入手,来看看里面做了什么。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout== null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
}
}
上面省略了一些无关代码,只看我们关心的,dispatchLayoutStep1和2方法,1方法中如果mState.mRunPredictiveAnimations为true会调用mLayout.onLayoutChildren(mRecycler, mState)方法,但是一般RV的预测动画都为false,所以看一下2方法,方法中同样调用了mLayout.onLayoutChildren(mRecycler, mState)方法,来看一下:
private void dispatchLayoutStep2() {
……
mLayout.onLayoutChildren(mRecycler, mState);
……
}
这里onLayoutChildren方法是必走的,mLayout就是RV的成员变量即LayoutManager,那么去LinearLayoutManager里看看onLayoutChildren方法做了什么。
@Override
public void onLayoutChildren( RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
fill(recycler, mLayoutState, state, false);
// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
} else {
// fill towards end
fill(recycler, mLayoutState, state, false);
// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
来看下detachAndScrapAttachedViews(recycler)方法中做了什么。
public void detachAndScrapAttachedViews( Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i–) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
如果有子view,则调用scrapOrRecycleView(recycler, i, v)方法,继续追踪:
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal( viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onVie wDetached(viewHolder);
}
}
正常开始布局的时候会进入else分支,首先调用detachViewAt(index)来分离视图,然后调用recycler.scrapView(view)方法。前面说过Recycler是RV的内部类,是管理RV缓存的核心类,然后继续追踪这个srapView方法,看看里面做了什么。
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags( ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException( “……”);
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
}
}
这里看到了熟悉的mAttachedScrap,现在知道了onLayoutChildren方法中调用detachAndScrapAttachedViews方法会把存在的子view先分离,然后缓存到了AttachedScrap中。
回到onLayoutChildren方法中看看接下来做了什么:
@Override
public void onLayoutChildren( RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
fill(recycler, mLayoutState, state, false);
// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
} else {
// fill towards end
fill(recycler, mLayoutState, state, false);
// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}
它先判断了方向,因为LinearLayoutManager有横纵两个方向,无论哪个方向最后都是调用fill方法,这是个填充布局的方法,fill方法中又调用了layoutChunk方法:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view = = null) {
return;
}
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout = = (layoutState.mLayoutDirection = = LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
}
该方法中通过layoutState.next(recycler)方法拿到视图,如果这个视图为null方法就终止,否则就会调用addView方法将视图添加或者重新attach回来,这个我们不关心,我们看看是怎么拿到视图的。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition( mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
如果mScrapList不为空就会去其中取视图,mScrapList是什么呢?实际上它就是mAttachedScrap,但是它是只读的,而且只有在开启预测动画时才会被赋值,所以忽略它即可。重点关注下recycler.getViewForPosition(mCurrentPosition)方法,这个方法经过层层调用,最终是调用的Recycler类中的tryGetViewHolderForPositionByDeadline(int position,boolean dryRun,long deadlineNs)方法,接下来看一下这个方法做了哪些事。
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition( position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder = = null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder = = null) {
// 2) Find from scrap/cache via stable ids, if exists
if (holder = = null && mViewCacheExtension != null) {
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
}
if (holder = = null) {
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder = = null) {
holder = mAdapter.createViewHolder( RecyclerView.this, type);
}
}
return holder;
}
这段代码做了很多事情,获取View和绑定View都是在这个方法中完成的,关于绑定和其它的无关代码这里就不贴了。具体看一下:
①第一步先从getChangedScrapViewForPosition( position)方法中找需要的视图,但是有个条件mState.isPreLayout()要为true,这个一般在调用adapter的notifyItemChanged等方法时为true,其实也很好理解,数据发生了变化,viewholder被detach掉后缓存在mChangedScrap之中,在这里拿到的viewHolder后续需要重新绑定。
②第二步,如果没有找到视图则从getScrapOrHiddenOrCachedHolderForPosition方法中继续找。这个方法的查找顺序为:
首先从mAttachedScrap中查找;再次从前面略过的ChildHelper类中的mHiddenViews中查找;最后是从mCachedViews中查找的。
③第三步, mViewCacheExtension中查找,这个对象默认是null的,是由开发者自定义缓存策略的一层,所以如果没有定义过,这里是找不到View的。
④第四步,从RecyclerPool中查找,先通过itemType从SparseArray类型的mscrap中拿到ScrapData,不为空继续拿到scrapHeap这个ArrayList,然后取到视图,这里拿到的视图需要重新绑定。
⑤第五步,如果前面几步都没有拿到视图,那么调用了mAdapter.createViewHolder( RecyclerView.this, type)方法,这个方法内部调用了一个抽象方法onCreateViewHolder,就是自定义一个Adapter要实现的方法之一。
到此,获取一个视图的流程就结束了,获取到视图之后就是怎么摆放视图并添加到RV之中,然后最终展示出来。细心的小伙伴可能发现这个流程貌似有点问题啊?第一次进入onLayoutChildren时还没有任何子view,在fill方法前等于没有缓存子view,所有的子View都是第五步onCreateViewHolder创建而来的。实际上这里的设计是有道理的,除了一些特殊情况onLayoutChildren方法会被多次调用外,一个View从无到有展示出来要至少经过两次onMeasure、一次onLayout和一次onDraw方法(为什么是这样的呢,感兴趣的小伙伴可以去ViewRootImpl中找找答案)。所以这里需要做个缓存,而不至于每次都重新创建新的视图。整个过程大致如图:
注意,在RV展示成功后,Scrap这层的缓存就为空了,在从Scrap中取视图的同时就被移出了缓存。在onLayout这里最终会调用到dispatchLayoutStep3方法,在3中,如果Scrap还有缓存,那么缓存会被清空,清空的缓存会被添加到mCachedViews或者RecyclerPool中。
(2)RV滑动时的缓存过程
RV是可以通过滚动来展示大量数据的控件,那么由当前屏幕滚动而出的View去哪了?滚动而入的View哪来的?同样去源码中找答案。
一个LayoutManager如果可以滑动,那么scrollHorizontallyBy和scrollVerticallyBy两个方法就要返回非0值,分别代表可以横向滚动和纵向滚动。最终两个方法都会调用scrollBy方法,然后scrollby方法调用fill方法,fill方法已经见过了,现看一下具体逻辑:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
这段代码中判断了当前是否是滚动触发的fill方法,如果是则调用recycleByLayoutState(recycler, layoutState)方法。这个方法几经周转会调用到removeAndRecycleViewAt方法:
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
这里注意先把视图remove掉了,而不是detach掉。然后调用Recycler中的recycleView方法,这个方法最后会调用recycleViewHolderInternal方法,方法如下:
void recycleViewHolderInternal(ViewHolder holder) {
if (forceRecycle || holder.isRecyclable()) {
if (省略) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize–;
}
mCachedViews.add( targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool( holder, true);
recycled = true;
}
}
}
删除不相关代码后逻辑很清晰。前面说过mCachedViews是有容量限制的,默认为2。如果符合放到mCachedViews中的条件,首先会判断mCachedViews是否已经满了,如果满了会通过recycleCachedViewAt(0)方法把最老的那个缓存放进RecyclerPool,然后再把新的视图放进mCachedViews中。如果这个视图不符合条件会直接被放进RecyclerPool中。注意,在缓存进mCachedViews之前,视图只是被remove掉了,绑定的数据等信息都还在,这意味着从mCachedViews取出的视图如果符合需要的目标视图是可以直接展示的,而不需要重新绑定。而放进RecyclerPool最终是要调用putRecycledView方法的。
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList< ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
这个方法中同样对容量做了判断,跟mCachedViews不一样,如果容量满了,就不再继续缓存了。在缓存之前先调用了scrap.resetInternal()方法,这是个重置的方法,缓存之前把视图的信息都清除掉了,这也是为什么这里缓存满了之后就不再继续缓存了,而不是把老的缓存替换掉,因为它们重置后都一样了(这里指具有同种itemType的是一样的)。这就是滑动缓存的全过程,现在知道了滚动出去的视图去哪了,那么滚动进来的视图哪来的呢?
和从无到有的过程一样,最后滚动也调用了fill方法,那最后必然是要走到前面分析的获取视图的那个流程。前面说过在布局完成之后,Scrap层的缓存就是空的了,那就只能从mCachedViews或者RecyclerPool中取了,都取不到最后就会走onCreateViewHolder创建视图。到这里滑动时的缓存以及取缓存就讲完了。
(3)数据更新时的缓存过程
为什么在有数据刷新的时候推荐大家使用notifyItemChanged等方法而不使用notifyDataSetChanged方法呢?
在调用notifyDataSetChanged方法后,所有的子view会被标记,这个标记导致它们最后都被缓存到RecyclerPool中,然后重新绑定数据。并且由于RecyclerPool有容量限制,如果不够最后就要重新创建新的视图了。
但是使用notifyItemChanged等方法会将视图缓存到mChangedScrap和mAttachedScrap中,这两个缓存是没有容量限制的,所以基本不会重新创建新的视图,只是mChangedScrap中的视图需要重新绑定一下。