RecycleView 使用解析

1.简介

RecyclerView的官方资料介绍是:A flexible view for providing a limited window into a large data set,大体意思就是RecyclerView是一个用于显示大量数据的弹性视图控件。在RecyclerView出现之前,我们往往使用ListView显示大量的数据,对于ListView,其官方介绍是:A view that shows items in a vertically scrolling list,即垂直显示一组数据,注意这里加入了垂直两个字,这也正是RecyclerView和ListView的一个非常直观的区别。使用RecylclerView能够很容易的实现水平、垂直、瀑布流等显示样式,而ListView只能进行垂直显示。究其原因在于,ListView把布局方式硬编码进ListView类中,而RecyclerView则把布局方式和自身解耦。

2.特性

1.封装了对viewholder的回收复用。

2.可以同时实现线性布局,网格布局,瀑布流布局,完美替代ListView+GrideView,更容易组合设计出自己需要的滑动布局。

3.自带了ItemAnimation,可以设置加载和移除时的动画,方便做出各种动态浏览的效果。

2.基本设计原理

​ RecyclerView本质是用来把View和Datas关联起来,将Data展示到View上。

​ RecyclerView的职责就是将Datas中的数据以一定的规则展示在它的上面,但说破天RecyclerView只是一个ViewGroup,它只认识View,不清楚Data数据的具体结构,所以两个陌生人之间想构建通话,我们很容易想到适配器模式,因此,RecyclerView需要一个Adapter来与Datas进行交流:

​ 如上如所示,RecyclerView表示只会和ViewHolder进行接触,而Adapter的工作就是将Data转换为RecyclerView认识的ViewHolder,因此RecyclerView就间接地接触了Datas。

​ 然而仍然不是特别完美,尽管Adapter已经将Datas转换为RecyclerView所熟知的View,但RecyclerView并不想自己管理些子View,因此,它雇佣了一个叫做LayoutManager的Manager来帮其完成布局,现在,图示变成下面这样:

​ 到了这里,有负责翻译数据的Adapter,有负责布局的LayoutManager,有负责管理View的Recycler,一切都很完美,但RecyclerView之所优雅,还在于当子View变动的时候姿态要优雅(动画),所以用需要了一个舞者ItemAnimator,因此,动画也进入了这个图示:

3.基本使用

1.创建对象

RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);

2.设置显示规则

recyclerview.setLayoutManager(new LinearLayoutManager(this));

​ RecyclerView 将所有的显示规则交给一个叫 LayoutManager 的类去完成了。
​ LayoutManager是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是 LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManager ,从名字我们就能看出来了,分别是,线性显示、网格显示、瀑布流显示。当然你也可以通过继承这些类来扩展实现自己的 LayougManager

3.设置适配器

recyclerview.setAdapter(adapter);

​ 适配器,同 ListView 一样,用来设置每个item显示内容的。
通常,我们写 ListView 适配器,都是首先继承 BaseAdapter ,实现四个抽象方法,创建一个静态 ViewHoldergetView() 方法中判断 convertView 是否为空,创建还是获取 viewholder 对象。
RecyclerView 也是类似的步骤,首先继承 RecyclerView.Adapter<VH> 类,实现三个抽象方法,创建一个静态的 ViewHolder ,并且 ViewHolder 必须继承自 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 int getItemViewType(int position) {
        return list.get(position).getType();
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
    }
  
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.mTextView.setText(dataList.get(position).getNum());
    }
  
    @Override
    public int getItemCount() {
        return dataList.size();
    }
  
    public static class ViewHolder extends RecyclerView.ViewHolder {
        TextView mTextView;
        public ViewHolder(View itemView) {
            super(itemView);
            mTextView = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
}
public class DemoAdapter extends BaseAdapter {

    private List<Student> stuList;
    private LayoutInflater inflater;

    public DemoAdapter(List<Student> stuList,Context context) {
        this.stuList = stuList;
        this.inflater=LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return stuList==null?0:stuList.size();
    }

    @Override
    public Student getItem(int position) {
        return stuList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //加载布局为一个视图
       ViewHolder holder;
        if(convertView == null){
            convertView = mInflater.inflate(R.layout.list_view, null);
            holder = new ViewHolder();
            holder.tv = (TextView)convertView.findViewById(R.id.text);
            holder.iv = (ImageView)convertView.findViewById(R.id.img);
            convertView.setTag(holder);
        }else{
            holder = (ViewHolder) convertView.getTag();
        }
        holder.tv.setText(DATA[position]);
        holder.iv.setImageBitmap(icon);
        return convertView;
    }
  
    static class ViewHolder{
        private TextView tv;
        private ImageView iv;
    }
ps:与Listview的Adapter对比:
  • .在Recycleview中封装了ViewHolder,即内部即支持复用,不用再手动判断ContentView及ViewHolder。
  • 增加ViewType概念,即不同的ViewType对应不同的View,实现不同View的Item复用
  • 将View的获取与数据填充解耦。

4.添加删除 item 的动画

​ 同 ListView每次修改了数据源后,都要调用 notifyDataSetChanged() 刷新每项 item 类似,只不过RecyclerView 还支持局部刷新 notifyItemInserted(index); notifyItemRemoved(position)notifyItemChanged(position)
​ 在添加或删除了数据后,RecyclerView还提供了一个默认的动画效果,来改变显示。同时,你也可以定制自己的动画效果:模仿 DefaultItemAnimator 或直接继承这个类,实现自己的动画效果,并调用recyclerview.setItemAnimator(new DefaultItemAnimator()); 设置上自己的动画。

5.Item Decoration

RecyclerView通过addItemDecoration()方法添加item之间的分割线。但是RecycleView并并没有提供实现好的Divider,因此任何分割线样式都需要自己实现。

方法是:创建一个类并继承RecyclerView.ItemDecoration,重写以下两个方法:

  • onDraw(): 绘制分割线。
  • getItemOffsets(): 设置分割线的宽、高。

6.点击事件

​ RecyCleView默认不支持任何点击或者长按事件,好处是可以自己上实现任意的点击事件或者任意的回调,缺点是什么都要自己写。

   
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = View.inflate(context, android.R.layout.simple_list_item_2, null)
          view.setOnclickListener(this);
        return new VH(view);
    }

    @Override
    public void onClick(View v) {
        if (mOnItemClickListener != null) {
            //设置该item被点击
            list.get((int)v.getTag()).setClicked(true);
            //注意这里使用getTag方法获取数据
            mOnItemClickListener.onItemClick(v,list.get((int)v.getTag()));
            notifyDataSetChanged();
        }
    }

    public interface OnRecyclerViewItemClickListener {
        void onItemClick(View view , NewsInfo data);
    }

5.缓存分析

​ 源码十分复杂,所以主要结合缓存机制来讲他的一些源代码。

​ 不管是ListView还是RecycleView,缓存机制大致可以描述为下图:View本身拥有一个缓存池,对于滑出屏幕的itemView,我们会将他放入缓存池中,在展示新的itemView时,会从缓存池中拿出可以服用的itemView。

4.1 ListView回收机制

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

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

接下来我们通过源码分析ListView是如何与RecycleBin交互的。其实ListView和RecyclerView的layout过程大同小异,ListView的布局函数是 layoutChildren(),实现如下:

   if (dataChanged) {
     //如果数据源改变,会将所有itemView回收至ScrapView,否则放至ActiveViews
     for (int i = 0; i < childCount; i++) {
       recycleBin.addScrapView(getChildAt(i), firstPosition+i);
     }
   } else {
     recycleBin.fillActiveViews(childCount, firstPosition);
   }

// Clear out old views
  detachAllViewsFromParent();
  recycleBin.removeSkippedScrap();

  switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSel != null) {
                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
                } else {
                    sel = fillFromMiddle(childrenTop, childrenBottom);
                }
                break;
            case LAYOUT_SYNC:
                sel = fillSpecific(mSyncPosition, mSpecificTop);
                break;
            case LAYOUT_FORCE_BOTTOM:
                sel = fillUp(mItemCount - 1, childrenBottom);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                sel = fillFromTop(childrenTop);
                adjustViewsUpOrDown();
                break;
            case LAYOUT_SPECIFIC:
                final int selectedPosition = reconcileSelectedPosition();
                sel = fillSpecific(selectedPosition, mSpecificTop);

​ 其中 fillXxx()实现了对Item View进行填充,这些方法内部都调用了 makeAndAddView(),实现如下:

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;
        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);

        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

其中, getActiveView()是从mActiveViews中获取合适的View,如果获取到了,则直接返回,而不调用 obtainView(),这也印证了如果从mActiveViews获取到了可复用的View,则不需要调用adapter中的 getView()

obtainView()是从mScrapViews中获取合适的View,然后以参数形式传给了 getView(),实现如下:

View obtainView(int position, boolean[] isScrap) {  
    isScrap[0] = false;  
    View scrapView;  
    scrapView = mRecycler.getScrapView(position);  
    View child;  
    if (scrapView != null) {  
        child = mAdapter.getView(position, scrapView, this);  
        if (child != scrapView) {  
            mRecycler.addScrapView(scrapView);  
            if (mCacheColorHint != 0) {  
                child.setDrawingCacheBackgroundColor(mCacheColorHint);  
            }  
        } else {  
            isScrap[0] = true;  
            dispatchFinishTemporaryDetach(child);  
        }  
    } else {  
        child = mAdapter.getView(position, null, this);  
        if (mCacheColorHint != 0) {  
            child.setDrawingCacheBackgroundColor(mCacheColorHint);  
        }  
    }  
    return child;  
}  

4.2 RecycleView 回收机制

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

  • mAttachedScrap: 缓存在屏幕上的ViewHolder。
  • mCachedViews: 缓存屏幕外的ViewHolder,默认为2个。ListView对于屏幕外的缓存都会调用 getView()
  • mViewCacheExtensions: 需要用户定制,默认不实现。
  • mRecyclerPool: 缓存池,多个RecyclerView共用。

RecycleView 的itemView回收同理也是主要在onLayout()中,主要代码如下:

@Override
protected void onLayout(boolean changed, int l, int t, 
  int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

void dispatchLayout() {
    ......
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        ......
        dispatchLayoutStep1();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) {

        ......
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

private void dispatchLayoutStep1(){   
  ...
    if (mState.mRunSimpleAnimations) {
      int count = mChildHelper.getChildCount();
      for (int i = 0; i < count; ++i) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
        final ItemHolderInfo animationInfo = mItemAnimator
          .recordPreLayoutInformation(mState, holder,
                                      ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                      holder.getUnmodifiedPayloads());
        mViewInfoStore.addToPreLayout(holder, animationInfo);
        ...
      }
    }
  ...
    mState.mLayoutStep = State.STEP_LAYOUT;
}

​ step的第一步目的就是在记录View的状态,首先遍历当前所有的View依次进行处理,mItemAnimator会根据每个View的信息封装成一个ItemHolderInfo,这个ItemHolderInfo中主要包含的就是当前View的位置状态等。

private void dispatchLayoutStep2() {
  ...
    mLayout.onLayoutChildren(mRecycler, mState);
  ...
    mState.mLayoutStep = State.STEP_ANIMATIONS;
}

​ layout的第二步主要就是真正的去布局View了,前面也说过,RecyclerView的布局是由LayoutManager负责的,所以第二步的主要工作也都在LayoutManager中,由于每种布局的方式不一样,这里我们以常见的LinearLayoutManager为例。

   public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
            
            ...
            if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
                    mPendingSavedState != null) {
                updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            }
            ...
        
            if (mAnchorInfo.mLayoutFromEnd) {
                firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
                        LayoutState.ITEM_DIRECTION_HEAD;
            } else {
                firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                        LayoutState.ITEM_DIRECTION_TAIL;
            }
            ...
            onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
        
            ...
            if (mAnchorInfo.mLayoutFromEnd) {
               ...
            } else {
                // fill towards end
                updateLayoutStateToFillEnd(mAnchorInfo);
                fill(recycler, mLayoutState, state, false);
                  ...
                  
                // fill towards start
                updateLayoutStateToFillStart(mAnchorInfo);
                  ...
                fill(recycler, mLayoutState, state, false);
                ...
            }
            ...
        }

​ 无论从上到下还是从下到上布局,都调用的是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) {
            recycleByLayoutState(recycler, layoutState);
        }
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
        }
        return start - layoutState.mAvailable;
     }

首先比较重要的函数是recycleByLayoutState,这个函数它会根据当前信息对不需要的View进行回收。

 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        ...
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
           ...
        }
        ...
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        ...
    }
    View next(RecyclerView.Recycler recycler) {
        ...
        final View view = recycler.getViewForPosition(mCurrentPosition);
        return view;
    }

最终的实现:

View getViewForPosition(int position, boolean dryRun) {
                boolean fromScrap = false;
                ViewHolder holder = null;
                if (mState.isPreLayout()) {
                    holder = getChangedScrapViewForPosition(position);
                    fromScrap = holder != null;
                }
                if (holder == null) {
                    holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
                   ...
                }
                if (holder == null) {
                    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                    
                    final int type = mAdapter.getItemViewType(offsetPosition);
                    if (mAdapter.hasStableIds()) {
                        holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                    }
                    if (holder == null && mViewCacheExtension != null) {
                        final View view = mViewCacheExtension
                                .getViewForPositionAndType(this, position, type);
                       ...
                    }
                    if (holder == null) { // fallback to recycler
                        holder = getRecycledViewPool().getRecycledView(type);
                        if (holder != null) {
                            holder.resetInternal();
                            if (FORCE_INVALIDATE_DISPLAY_LIST) {
                                invalidateDisplayListInt(holder);
                            }
                        }
                    }
                    if (holder == null) {
                        holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    }
                }
               //生成LayoutParams的代码 ...
                return holder.itemView;
            }
    }

查找的逻辑:

  • 搜索mChangedScrap,如果找到则返回相应holder。
  • 搜索mAttachedScrap与mCachedViews,如果找到且holder有效则返回相应holder。
  • 如果设置了mViewCacheExtension,对其调用getViewForPositionAndType方法进行获取,若该方法返回结果则生成对应的holder。
  • 搜索mRecyclerPool,如果找到到则返回相应holder
  • 如果上述过程都没有找到对应的holder,则执行我们熟悉的Adapter.createViewHolder(),创建新的holder实例

5.使用情景

ListView RecyCleView
使用场景 业务简单,布局简单,数据简单 1.列表需要动画支持2.频繁的更新3.局部更新4.多布局5.数据庞大

6.待整理问题

  • header和footer的合适解决方案,可以动态更换
  • 瀑布流
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,421评论 25 707
  • 这篇文章分三个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与ListView比...
    LucasAdam阅读 4,374评论 0 27
  • Tangram是阿里出品、用于快速实现组合布局的框架模型,在手机天猫Android&iOS版 内广泛使用 该框架提...
    wintersweett阅读 3,249评论 0 1
  • 宝贝13个半月,虽然除了爸爸妈妈之外,还不会说话,但她已经表现出对事物强烈的偏好。她最擅长会用金手指,看到喜欢激动...
    兔思思阅读 188评论 0 1
  • 2.暂时性死区 暂时性死区是指只要块级作用域内存在let命令,它所声明的变量就绑定这个作用域,不会受到外部的影响。...
    博为峰51Code教研组阅读 236评论 0 0