Recyclerview

介绍

从Android 5.0开始,谷歌公司推出了一个用于大量数据展示的新控件RecylerView,可以用来代替传统的ListView,更加强大和灵活。
RecyclerView的官方定义 A flexible view for providing a limited window into a large data set. 一种灵活的视图,用于为大型数据集提供有限的窗口。
从定义可以看出,flexible(可扩展性)是RecyclerView的特点。
RecyclerView是support-v7包中的组件现在已经移动到了androidx.recyclerview.widget,是一个强大的滑动组件,与经典的ListView相比,同样拥有item回收复用的功能,这一点从它的名字Recyclerview即回收view也可以看出。

用处

这是一个强大的滑动组件,用于在视图展示一个可滑动的列表。支持多种样式的列表 -> 横竖、网格和瀑布流。

优点

  • RecyclerView封装了Viewholder的回收复用,也就是说RecyclerView标准化了Viewholder,编写Adapter面向的是Viewholder而不再是View了,复用的逻辑被封装了,写起来更加简单。
  • 提供了一种插拔式的体验,高度的解耦,异常的灵活,针对一个Item的显示RecyclerView专门抽取出了相应的类,来控制Item的显示,使其的扩展性非常强。
  • 设置布局管理器以控制Item的布局方式,横向、竖向以及瀑布流方式,也就是说RecyclerView不再拘泥于ListView的线性展示方式,它也可以实现GridView的效果等多种效果。
  • 可设置Item的间隔样式(可绘制),通过继承RecyclerViewItemDecoration这个类,然后针对自己的业务需求去书写代码。
  • 可以控制Item增删的动画,可以通过ItemAnimator这个类进行控制,当然针对增删的动画,RecyclerView有其自己默认的实现。

原理

RecyclerView的基本使用

recyclerView = (RecyclerView) findViewById(R.id.recyclerView);  
LinearLayoutManager layoutManager = new LinearLayoutManager(this);  
//设置布局管理器  
recyclerView.setLayoutManager(layoutManager);  
//设置为垂直布局,这也是默认的  
layoutManager.setOrientation(OrientationHelper. VERTICAL);  
//设置Adapter  
recyclerView.setAdapter(recycleAdapter);  
 //设置分隔线  
recyclerView.addItemDecoration(new DividerGridItemDecoration(this));  
//设置增加或删除条目的动画  
recyclerView.setItemAnimator(new DefaultItemAnimator());  

在使用RecyclerView时候,必须指定一个适配器Adapter和一个布局管理器LayoutManager。适配器继承RecyclerView.Adapter类,具体实现类似ListView的适配器,取决于数据信息以及展示的UI。布局管理器用于确定RecyclerView中Item的展示方式以及决定何时复用已经不可见的Item,避免重复创建以及执行高成本的findViewById()方法。
可以看见RecyclerView相比ListView会多出许多操作,这也是RecyclerView灵活的地方,它将许多动能暴露出来,用户可以选择性的自定义属性以满足需求。

四大组成

  • Layout Manager:Item的布局。
  • Adapter:为Item提供数据。
  • Item Decoration:Item之间的Divider。
  • Item Animator:添加、删除Item动画。

Layout Manager布局管理器

RecyclerView能够支持各种各样的布局效果,这是 ListView所不具有的功能,那么这个功能如何实现的呢?其核心关键在于 RecyclerView.LayoutManager 类中。从前面的基础使用可以看到,RecyclerView在使用过程中要比 ListView多一个 setLayoutManager 步骤,这个 LayoutManager就是用于控制我们 RecyclerView最终的展示效果的。
LayoutManager负责 RecyclerView的布局,其中包含了Item View的获取与回收。
RecyclerView提供了三种布局管理器:

  • LinerLayoutManager 以垂直或者水平列表方式展示Item
  • GridLayoutManager 以网格方式展示Item
  • StaggeredGridLayoutManager 以瀑布流方式展示Item

如果你想用 RecyclerView来实现自己自定义效果,则应该去继承实现自己的 LayoutManager,并重写相应的方法,而不应该想着去改写 RecyclerView
对于LinearLayoutManager来说,比较重要的几个方法有:
onLayoutChildren(): 对RecyclerView进行布局的入口方法
fill(): 负责填充RecyclerView
scrollVerticallyBy():根据手指的移动滑动一定距离,并调用fill()填充
canScrollVertically()或canScrollHorizontally(): 判断是否支持纵向滑动或横向滑动
fill()是对剩余空间不断地调用layoutChunk(),直到填充完为止。

//SDK 28
public void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) { 
    View view = layoutState.next(recycler);//调用了getViewForPosition()
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    //添加View
    if (layoutState.mScrapList == null) {
        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); //计算View的大小
    ...,
    // We calculate everything with View's bounding box (which includes decor and margins)
    // To calculate correct layout position, we subtract margins.
    layoutDecoratedWithMargins(view, left, top, right, bottom);//布局View
    ...,
}

其中next()调用了getViewForPosition(currentPosition),该方法是从RecyclerView的回收机制实现类Recycler中获取合适的View,在后文的回收机制中会介绍该方法的具体实现。

Adapter适配器和ViewHolder

Adapter的功能就是为RecyclerView提供数据,为了创建一个RecyclerView的Adapter,和ListView的Adapter类似,都是用来展示和绑定ItemView的。
主要方法

  • onCreateViewHolder(@NonNull ViewGroup parent, int viewType):创建ViewHolder
  • onBindViewHolder(@NonNull VH holder, int position):绑定ViewHolder
  • getItemCount():列表数量
    Adapter主要是操作ViewHolder,并不是直接操作View视图,根据方法去创建和绑定ViewHolder,让ViewHodler去根据数据操作视图。

Item Decoration

RecyclerView通过addItemDecoration()方法添加item之间的分割线。Android并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。
自定义间隔样式需要继承RecyclerView.ItemDecoration类,该类是个抽象类,官方目前并没有提供默认的实现类,主要有三个方法。

  • onDraw(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
  • onDrawOver(Canvas c, RecyclerView parent, State state),在Item绘制之前被调用,该方法主要用于绘制间隔样式。
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state),设置item的偏移量,偏移的部分用于填充间隔样式,即设置分割线的宽、高;在RecyclerView的onMesure()中会调用该方法。
  • onDraw()和onDrawOver()这两个方法都是用于绘制间隔样式,我们只需要复写其中一个方法即可。

Item Animator

RecyclerView能够通过RecyclerView.setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果。
RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。如果没有特殊的需求,默认使用这个动画即可。
DefaultItemAnimator继承自SimpleItemAnimator,SimpleItemAnimator继承自ItemAnimator。
首先我们介绍ItemAnimator类的几个重要方法:

  • animateAppearance(): 当ViewHolder出现在屏幕上时被调用(可能是add或move)。
  • animateDisappearance(): 当ViewHolder消失在屏幕上时被调用(可能是remove或move)。
  • animatePersistence(): 在没调用notifyItemChanged()和notifyDataSetChanged()的情况下布局发生改变时被调用。
  • animateChange(): 在显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。
  • runPendingAnimations(): RecyclerView动画的执行方式并不是立即执行,而是每帧执行一次,比如两帧之间添加了多个Item,则会将这些将要执行的动画Pending住,保存在成员变量中,等到下一帧一起执行。该方法执行的前提是前面animateXxx()返回true。
  • isRunning(): 是否有动画要执行或正在执行。
  • dispatchAnimationsFinished(): 当全部动画执行完毕时被调用。

SimpleItemAnimator类(继承自ItemAnimator),该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可:

  • animateAdd(ViewHolder holder): 当Item添加时被调用。
  • animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 当Item移动时被调用。
  • animateRemove(ViewHolder holder): 当Item删除时被调用。
  • animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 当显式调用notifyItemChanged()或notifyDataSetChanged()时被调用。

以上四个方法,注意两点:
当Xxx动画开始执行前(在runPendingAnimations()中)需要调用dispatchXxxStarting(holder),执行完后需要调用dispatchXxxFinished(holder)。
这些方法的内部实际上并不是书写执行动画的代码,而是将需要执行动画的Item全部存入成员变量中,并且返回值为true,然后在runPendingAnimations()中一并执行。

嵌套滑动机制

Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题。
为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。

提问

1.RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理
实现方法有很多:

  • 可以监听RecyclerView的Touch事件然后判断手势做相应的处理,
  • 也可以通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去

2.局部刷新闪屏问题
对于RecyclerView的Item Animator,有一个常见的坑就是“闪屏问题”。
这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()时,文字改变的同时图片会闪一下。这个问题的原因是当调用notifyItemChanged()时,会调用DefaultItemAnimator的animateChangeImpl()执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。
解决办法很简单,在rv.setAdapter()之前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)禁用change动画。

RecyclerView vs ListView

ListView相比RecyclerView,有一些优点:

  • addHeaderView(), addFooterView()添加头视图和尾视图。
  • 通过android:divider设置自定义分割线。
  • setOnItemClickListener()和setOnItemLongClickListener()设置点击事件和长按事件。这些功能在RecyclerView中都没有直接的接口,要自己实现(虽然实现起来很简单),因此如果只是实现简单的显示功能,ListView无疑更简单。

RecyclerView相比ListView,有一些明显的优点:

  • 默认已经实现了View的复用,不需要类似if(convertView == null)的实现,而且回收机制更加完善。
  • 默认支持局部刷新。
  • 容易实现添加item、删除item的动画效果。
  • 容易实现拖拽、侧滑删除等功能。
  • 配置不同的LayoutManaer可以实现不同的布局效果,而ListView只能实现垂直列表。

RecyclerView是一个插件式的实现,对各个功能进行解耦,从而扩展性比较好。

局部刷新

ListView实现局部刷新
我们都知道ListView通过adapter.notifyDataSetChanged()实现ListView的更新,这种更新方法的缺点是全局更新,即对每个Item View都进行重绘。但事实上很多时候,我们只是更新了其中一个Item的数据,其他Item其实可以不需要重绘。
当然ListView也可以实现局部刷新

public void updateItemView(ListView listview, int position, Data data){
    int firstPos = listview.getFirstVisiblePosition();
    int lastPos = listview.getLastVisiblePosition();
    if(position >= firstPos && position <= lastPos){  //可见才更新,不可见则在getView()时更新
        //listview.getChildAt(i)获得的是当前可见的第i个item的view
        View view = listview.getChildAt(position - firstPos);
        VH vh = (VH)view.getTag();
        vh.text.setText(data.text);
    }
}

通过ListView的getChildAt()来获得需要更新的View,然后通过getTag()获得ViewHolder,从而实现更新。

RecyclerView实现局部刷新

RecyclerView提供了notifyItemInserted(),notifyItemRemoved(),notifyItemChanged()等API更新单个或某个范围的Item视图。

缓存机制对比

ListView与RecyclerView缓存机制原理大致相似

缓存和复用.png

在列表滑动的过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异。
1.层级不同
RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。RecyclerView的缓存在Recycler类中。
ListView
ListView缓存.png

RecyclerView
RecyclerView缓存.png

ListView和RecyclerView缓存机制基本一致:

  • mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;
  • mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.
  • RecyclerView的优势在于
    1).mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;
    2).mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势。

客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

2.缓存不同

  • RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为:
    View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);
  • ListView缓存View。

缓存不同,二者在缓存的使用上也略有差别,具体来说:
ListView获取缓存的流程:


ListView读取缓存.png

RecyclerView获取缓存的流程:


RecyclerView读取缓存.png
  • RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView。
    而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。
//AbsListView源码:line2365 SDK28
View obtainView(int position, boolean[] outMetadata) {
    //通过匹配pos从mScrapView中获取缓存
    final View scrapView = mRecycler.getScrapView(position);
    //无论是否成功都直接调用getView,导致必定会调用createView
    final View child = mAdapter.getView(position, scrapView, this);
    if (scrapView != null) {
        if (child != scrapView) {
        mRecycler.addScrapView(scrapView, position);
        } else {
        ...
        }
    }
}

//CursorAdapter为例
public View getView(int position, View convertView, ViewGroup parent) {
    ...
    View v;
    if (convertView == null) {
        v = newView(mContext, mCursor, parent);
    } else {
        v = convertView;
    }
    bindView(v, mContext, mCursor);
    return v;
}
  • ListView中通过pos获取的是view,即pos–>view;
    RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag);
    从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心。

局部刷新

由上文可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView。
结合RecyclerView的缓存机制,看看局部刷新是如何实现的:
以RecyclerView中notifyItemRemoved(1)为例,最终会调用requestLayout(),使整个RecyclerView重新绘制,过程为:
onMeasure()–>onLayout()–>onDraw()
其中,onLayout()为重点,分为三步:

  • dispathLayoutStep1():记录RecyclerView刷新前列表项ItemView的各种信息,如Top,Left,Bottom,Right,用于动画的相关计算;
  • dispathLayoutStep2():真正测量布局大小,位置,核心函数为layoutChildren();
  • dispathLayoutStep3():计算布局前后各个ItemView的状态,如Remove,Add,Move,Update等,如有必要执行相应的动画.


    layoutChildren流程图.png

当调用notifyItemRemoved时,会对屏幕内ItemView做预处理,修改ItemView相应的pos以及flag(流程图中红色部分):


1.png

当调用fill()中RecyclerView.getViewForPosition(pos)时,RecyclerView通过对pos和flag的预处理,使得bindview只调用一次.
需要指出,ListView和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

回收机制源码分析

ListView回收机制
ListView为了保证Item View的复用,实现了一套回收机制,该回收机制的实现类是RecycleBin,他实现了两级缓存:

  • View[] mActiveViews: 缓存屏幕上的View,在该缓存里的View不需要调用getView()。
  • ArrayList[] mScrapViews;: 每个Item Type对应一个列表作为回收站,缓存由于滚动而消失的View,此处的View如果被复用,会以参数的形式传给getView()。

ListView和RecyclerView的layout过程大同小异,ListView的布局函数是layoutChildren()

void layoutChildren(){
    //1. 如果数据被改变了,则将所有Item View回收至scrapView  
  //(而RecyclerView会根据情况放入Scrap Heap或RecyclePool);否则回收至mActiveViews
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }
    //2. 填充
    switch(){
        case LAYOUT_XXX:
            fillXxx();
            break;
        case LAYOUT_XXX:
            fillXxx();
            break;
    }
    //3. 回收多余的activeView
    mRecycler.scrapActiveViews();
}

其中fillXxx()实现了对Item View进行填充,该方法内部调用了makeAndAddView()。

View makeAndAddView(){
    if (!mDataChanged) {
        child = mRecycler.getActiveView(position);
        if (child != null) {
            return child;
        }
    }
    child = obtainView(position, mIsScrap);
    return child;
}

其中,getActiveView()是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用obtainView(),这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用getView()。
obtainView()是从mScrapViews中获取合适的View,然后以参数形式传给了getView()。

View obtainView(int position){
    final View scrapView = mRecycler.getScrapView(position);  //从RecycleBin中获取复用的View
    final View child = mAdapter.getView(position, scrapView, this);
}

getScrapView(position)的实现,该方法通过position得到Item Type,然后根据Item Type从mScrapViews获取可复用的View,如果获取不到,则返回null。

class RecycleBin{
    private View[] mActiveViews;    //存储屏幕上的View
    private ArrayList[] mScrapViews;  //每个item type对应一个ArrayList
    private int mViewTypeCount;            //item type的个数
    private ArrayList mCurrentScrap;  //mScrapViews[0]

    View getScrapView(int position) {
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap < 0) {
            return null;
        }
        if (mViewTypeCount == 1) {
            return retrieveFromScrap(mCurrentScrap, position);
        } else if (whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
        return null;
    }
    private View retrieveFromScrap(ArrayList scrapViews, int position){
        int size = scrapViews.size();
        if(size > 0){
            return scrapView.remove(scrapViews.size() - 1);  //从回收列表中取出最后一个元素复用
        } else{
            return null;
        }
    }
}

RecyclerView回收机制
RecyclerView和ListView的回收机制非常相似,但是ListView是以View作为单位进行回收,RecyclerView是以ViewHolder作为单位进行回收。
Recycler是RecyclerView回收机制的实现类,他实现了四级缓存:

  • mAttachedScrap: 缓存在屏幕上的ViewHolder。
  • mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用getView()。
  • mViewCacheExtensions: 需要用户定制,默认不实现。
  • mRecyclerPool: 缓存池,多个RecyclerView共用。
//参考SDK28
View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    ...
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    if (mState.isPreLayout()) {
        //第一个指向的是mChangedScrap缓存列表,如果有一个变化的ViewHolder,那么会从这个列表中获取,会执行bindViewHodler方法
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    //从mAttachedScrap,mCachedViews获取ViewHolder 1),2)
    // 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
       holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
        type, dryRun); 
    }
    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); //调用onCreateViewHolder()
    }
    if(!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
        mAdapter.bindViewHolder(holder, offsetPosition);
    }
    return holder.itemView;
}

依次从mChangedScrap,mAttachedScrap, mCachedViews, mViewCacheExtension, mRecyclerPool寻找可复用的ViewHolder,如果是从mAttachedScrap或mCachedViews中获取的ViewHolder,则不会调用onBindViewHolder(),mAttachedScrap和mCachedViews也就是我们所说的Scrap Heap;而如果从mChangedScrap、mViewCacheExtension或mRecyclerPool中获取的ViewHolder,则会调用onBindViewHolder()。
RecyclerView局部刷新的实现原理也是基于RecyclerView的回收机制,即能直接复用的ViewHolder就不调用onBindViewHolder()。

结论

  • 在一些场景下,如界面初始化,滑动等,ListView和RecyclerView都能很好地工作,两者并没有很大的差异;
  • 数据源频繁更新的场景,如弹幕等RecyclerView的优势会非常明显;
  • 列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用RecyclerView,更加强大完善,易扩展;其他情况如果对列表没有很大的要求,ListView在使用上会更加方便,快捷。

你可能感兴趣的:(Recyclerview)