Recyclerview

2020-04-20  本文已影响0人  G_Freedom

介绍

从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的基本使用

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布局管理器

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

如果你想用 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的。
主要方法

Item Decoration

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

Item Animator

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

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

以上四个方法,注意两点:
当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的点击和长按等事件的处理
实现方法有很多:

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,有一些优点:

RecyclerView相比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缓存机制基本一致:

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

2.缓存不同

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


ListView读取缓存.png

RecyclerView获取缓存的流程:


RecyclerView读取缓存.png
//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;
}

局部刷新

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

当调用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,他实现了两级缓存:

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回收机制的实现类,他实现了四级缓存:

//参考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()。

结论

上一篇下一篇

猜你喜欢

热点阅读