美文网首页
Recyclerview

Recyclerview

作者: G_Freedom | 来源:发表于2020-04-20 18:00 被阅读0次

    介绍

    从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<View>[] 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<View>[] mScrapViews;  //每个item type对应一个ArrayList
        private int mViewTypeCount;            //item type的个数
        private ArrayList<View> 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<View> 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

          本文链接:https://www.haomeiwen.com/subject/rrwdihtx.html