美文网首页
ListView和RecyclerView详解

ListView和RecyclerView详解

作者: Android开发_Hua | 来源:发表于2020-10-11 23:04 被阅读0次

           刚接触Android开发同学可能很早就使用过ListView这个控件了,在我们做滑动列表的时候一般都会用到,然而到了Android 5.0,谷歌在新的系统特性中,提到了Matrial Design的设计风格,并且伴随着提供了一系列相关的控件,RecyclerView就是其中的控件,用过的同学都说RecyclerView可以直接替代Listview使用,是ListView的升级版,下面本人就从各个方面比较这两个控件,看看他们的区别?

    知识点汇总:

    一:Listview与RecyclerView实现原理的区别

    二:Listview与RecyclerView接口使用的区别

    三:Listview与RecyclerView优化手段及相关知识点解析

    四:相关开源框架介绍与Rv控件其他UI实现

    五:扩展阅读

    一:Listview与RecyclerView实现原理的区别

    区别一:缓存机制不同

           RecyclerView比ListView多两级缓存,支持多个离开屏幕ItemView缓存,支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool(缓存池)。

    具体来说: ListView(两级缓存):

    RecyclerView的四级缓存实现:

    ListView和RecyclerView缓存机制对比:

    1、mActiveViews和mAttachedScrap功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新createView和bindView;

    2、mScrapView和mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的ItemView,目的是让即将进入屏幕的ItemView重用.

    3、RecyclerView的优势在于a.mCacheViews的使用,可以做到屏幕外的列表项ItemView进入屏幕内时也无须bindView快速重用;b.mRecyclerPool可以供多个RecyclerView共同使用,在特定场景下,如viewpaper+多个列表页下有优势.客观来说,RecyclerView在特定场景下对ListView的缓存机制做了补强和完善。

    缓存不同:

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

    2、ListView缓存View。

    ListView获取缓存的流程:

    RecyclerView获取缓存的流程:

    区别二:缓存对象不同

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

    2、ListView缓存View。

    获取缓存对比:

    1、 RecyclerView中mCacheViews(屏幕外)获取缓存时,是通过匹配pos获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新bindView,而同样是离屏缓存,ListView从mScrapViews根据pos获取相应的缓存,但是并没有直接使用,而是重新getView(即必定会重新bindView)。

    2、 ListView中通过pos获取的是view,即pos–>view; RecyclerView中通过pos获取的是viewholder,即pos –> (view,viewHolder,flag); 从流程图中可以看出,标志flag的作用是判断view是否需要重新bindView,这也是RecyclerView实现局部刷新的一个核心。

    3、局部刷新:

      由上可知,RecyclerView的缓存机制确实更加完善,但还不算质的变化,RecyclerView更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的bindView,ListView和RecyclerView在数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

    区别三:数据源改变时的处理

         ListView和RecyclerView在数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的mActiveViews都移入了二级缓存mScrapViews,而RecyclerView则是更加灵活地对每个View修改标志位,区分是否重新bindView。

    区别四:预取功能(Prefetch)

    解析:Android是通过每16ms刷新一次页面来保证ui的流畅程度,现在android系统中刷新ui会通过cpu产生数据,然后交给gpu渲染的形式来完成,从上图可以看出当cpu完成数据处理交给gpu后,就一直处于空闲状态,需要等待下一帧才会进行数据处理,rv会预取接下来可能要显示的item,在下一帧到来之前提前处理完数据,然后将得到的itemholder缓存起来,等到真正要使用的时候直接从缓存取出来即可。

    接口:通过LinearLayoutManager的setInitialItemPrefetchCount()。

    区别五:Item回收/复用方面

    解析:ListView是以convertView作为回收单位,需要手动添加ViewHolder,需要复用时常常需要设置Tag,而RecyclerView则是以ViewHolder作为回收单位,convertView被内置到了ViewHolder中作为ViewHolder的成员变量,内置了Recycle、多级缓存。

    二:Listview与RecyclerView接口使用的区别

    Listview:

    1、使用setAdapter,重载getView()函数,getItemCount()和getItemType()函数。

    RecyclerView:

    1、setLayoutManager()。

    2、setAdapter(),重载onBindViewHolder(),onCreateViewHolder。

    3、addItemDecoration(),getItemOffset函数。

    4、setItemAnimator()。

    5、SnapHelper。

           由于大家对ListView的使用已经比较熟悉了,这里就不描述了,后面优化部分会有Listview的代码示例,到时去一一了解,这里直接说关于RecyclerView的接口描述。

    RecyclerView及相关成员介绍:

    1、RecyclerView.Adapter - 处理数据集合并负责绑定视图

    2、ViewHolder - 持有所有的用于绑定数据或者需要操作的View

    3、LayoutManager - 负责摆放视图等相关操作

    4、ItemDecoration - 负责绘制Item附近的分割线

    5、ItemAnimator - 为Item的一般操作添加动画效果,如,增删条目等

    图例:

    Adapter类解析

    1、根据不同ViewType创建与之相应的的Item-Layout。

    2、访问数据集合并将数据绑定到正确的View上。

    常用方法:

     一般常用的重写方法有以下这么几个:

    1、public VH onCreateViewHolder(ViewGroup parent, int viewType)

        创建Item视图,并返回相应的ViewHolder

    2、public void onBindViewHolder(VH holder, int position)

        绑定数据到正确的Item视图上。

    3、public int getItemCount()

        返回该Adapter所持有的Itme数量

    4、public int getItemViewType(int position)

        用来获取当前项Item(position参数)是哪种类型的布局

    ViewHolder类解析:

    作用:

    1、adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作。

    2、其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型时,调用item的ViewHolder来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。

    ViewHolder类解析:

    作用:

    1、adapter应当拥有ViewHolder的子类,并且ViewHolder内部应当存储一些子view,避免时间代价很大的findViewById操作。

    2、其RecyclerView内部定义的ViewHolder类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是onCreateViewHolder()方法和onBindViewHolder()方法,onCreateViewHolder()方法在RecyclerView需要一个新类型时,调用item的ViewHolder来创建一个ViewHolder,而onBindViewHolder()方法则当RecyclerView需要在特定位置的item展示数据时调用。

    复用机制怎样的?

    1、模拟场景:只有一种ViewType,上下滑动的时候需要的ViewHolder种类是只有一种,但是需要的ViewHolder对象数量并不止一个。所以在后面创建了9个ViewHolder之后,需要的数量够了,无论怎么滑动,都只需要复用是以前创建的对象就行了。

    滑动列表时,log的输出:

    对于ViewHolder对象的数量“够用”之后就停止调用onCreateViewHolder方法:

    1、获取为给定位置初始化的视图。

    2‘此方法应由{@link LayoutManager}实现使用,以获取视图来表示来自Adapter的数据。

    3、如果共享池可用于正确的视图类型,则回收程序可以重用共享池中的废视图或分离视图。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试返回一个以前为该数据初始化的报废视图,而不进行重新绑定。

    LayoutManager类解析:

    作用:

    1、LayoutManager的职责是摆放Item的位置,并且负责决定何时回收和重用Item。

    2、RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager。

    LayoutManager样式有哪些?

    1、LinearLayoutManager 水平或者垂直的Item视图。

    2、GridLayoutManager 网格Item视图。

    3、StaggeredGridLayoutManager 交错的网格Item视图。

    SnapHelper类解析:

    作用:

           在某些场景下,卡片列表滑动浏览[有的叫轮播图],希望当滑动停止时可以将当前卡片停留在屏幕某个位置,比如停在左边,以吸引用户的焦点。那么可以使用RecyclerView + Snaphelper来实现。

    LinearSnapHelper类分析:

            LinearSnapHelper 使当前Item居中显示,常用场景是横向的RecyclerView,类似ViewPager效果,但是又可以快速滑动(滑动多页)。

    PagerSnapHelper类分析:

           PagerSnapHelper看名字可能就能猜到,使RecyclerView像ViewPager一样的效果,每次只能滑动一页(LinearSnapHelper支持快速滑动), PagerSnapHelper也是Item居中对齐。

    ItemDecoration类解析:

    作用:

    1、通过设置recyclerView.addItemDecoration(new DividerDecoration(this));来改变Item之间的偏移量或者对Item进行装饰。

    2、当然,你也可以对RecyclerView设置多个ItemDecoration,列表展示的时候会遍历所有的ItemDecoration并调用里面的绘制方法,对Item进行装饰。

    自定义ItemDecoration有哪些重写方法 :

    1、public void onDraw(Canvas c, RecyclerView parent)

    解析:装饰的绘制在Item条目绘制之前调用,所以这有可能被Item的内容所遮挡。

    2、public void  onDrawOver(Canvas c, RecyclerView parent)

    解析:装饰的绘制在Item条目绘制之后调用,因此装饰将浮于Item之上。

    3、public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)解析:与padding或margin类似,LayoutManager在测量阶段会调用该方法,计算出每一个Item的正确尺寸并设置偏移量。

    三:Listview与RecyclerView优化手段及相关知识点

    Listview的优化:

    1、局部刷新

    2、复用ViewHolder

    3、Item的布局优化(尝试使用constrantlayout)

    4、getView函数,注意尽量不用做创建对象的操作

    5、getView函数,注意尽量不用做复杂运算操作

    6、在滑动列表时,停止图片的动态加载(需要配合图片动态请求库)

    RecyclerView的优化:(ListView大部分优化点也适用与rv)

    1、setHasFixSize使用(嵌套RecyclerView)

    2、RecyclerViewPool的使用(嵌套RecyclerView)

    3、去除默认动画效果(如果不需要)

    4、设置点击事件(不算优化点)

    5、diffUtils的使用

    Listview的优化:

    下面就展示一下, 通用getView的代码示例

    @Override 

        public View getView(int position, View convertView, ViewGroup viewGroup) {

            //不同类型的ViewHolder

            TitleViewHolder titleViewHolder = null;

            CompanyViewHolder contentViewHolder = null;

            //对类型进行判断,分别inflate不同的布局

            switch (getItemViewType(position)) {

                case TYPE_TITLE:

                    titleViewHolder = new TitleViewHolder();

                    if (convertView == null) {

                        convertView = View.inflate(context, R.layout.view_holder_company_index, null);

                        titleViewHolder.title = (TextView) convertView.findViewById(R.id.tv_title);

                        convertView.setTag(titleViewHolder);                       //setTag()

                    } else {

                        titleViewHolder = (TitleViewHolder) convertView.getTag();  //getTag();

                    }

                    titleViewHolder.title.setText(mData.get(position).getName());

                    break;

                case TYPE_CONTENT:

                    contentViewHolder = new CompanyViewHolder();

                    if (convertView == null) {

                        convertView = View.inflate(context, R.layout.view_holder_company, null);

                        contentViewHolder.content = (TextView) convertView.findViewById(R.id.tv_content);

                        convertView.setTag(contentViewHolder);

                    } else {

                        contentViewHolder = (CompanyViewHolder) convertView.getTag();

                    }

                    contentViewHolder.content.setText(mData.get(position).getCode());

                    break;

            }

            return convertView;

        }

           我们知道listview并没有提供给我们局部刷新的解接口,可能谷歌后面也意识到这个问题,在2012年的时候,官方也提供了listview局部刷新的代码示例:

    google官方局部刷新代码:

    private void updateSingleRow(ListView listView, long id) {

            if (listView != null) {

                int start = listView.getFirstVisiblePosition();

                for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)

                    if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {

                        View view = listView.getChildAt(i - start);

                        getView(i, view, listView);

                        break;

                    }

            }

        }

    总结:核心就是找出你要更新Item的contentView.然后再去操作。因为ListView默认只会加载一屏的数据,所以要判断其可见范围。不可见的在滑动的时候getView会自动调用更新数据,最后要强调的一点就是关于布局优化,最好将item的高度设置为一个固定的值,这样能减少getView的调用次数。因为一个不确定的值,ListView会频繁调用多次getView去确定其高度和渲染。

    下面也有一些热心网友也提供了相关的局部刷新代码,大体思路其实也是一致的。

    局部刷新实现:

    public void updateSingleRow(ListView mListView, int posi) {

            if (mListView != null) {

                //获取第一个显示的item

                int visiblePos = mListView.getFirstVisiblePosition();

                //计算出当前选中的position和第一个的差,也就是当前在屏幕中的item位置

                int offset = posi - visiblePos;

                int lenth = mListView.getChildCount();

                // 只有在可见区域才更新,因为不在可见区域得不到Tag,会出现空指针,所以这              是必须有的一个步骤

                if ((offset < 0) || (offset >= lenth)) return;

                View convertView = mListView.getChildAt(offset);

                ViewHolder viewHolder = (ViewHolder) convertView.getTag();

                //以下是处理需要的控件方法。。。。。

            }

        }

    RecyclerView的优化及相关知识点:

    优化一:setHasFixedSize的使用(嵌套RecyclerView)

    解析:设置固定高度的rv,避免rv重复measure调用,当item嵌套了rv,并且rv没有设置wrap_content属性时,我们可以对该rv设置setHasFixedSize,这么做的一个最大的好处就是嵌套的rv不会触发requestLayout,从而不会导致外层的rv进行重绘。

    优化二:复用pool缓存(嵌套RecyclerView)

    使用场景:

    1、一个Rv嵌套多个Rv。

    2、ViewPager中多个界面都有相同布局的Tv。

       我们看到上面斗鱼App的两张图片UI,在不同的TAG,列表布局几乎完全一样,那么我们就可以尝试复用RecyclerView了。

       解析:

       由于ViewPager默认缓存左右两个fragment,所以进行fragment切换的时候,每次tab0和tab2都会重新生成,对应的onCreateViewHolder和onBindViewHolder都会执行。

           同样的操作,因为公用了RecycledViewPool,所以切换过程中只调用了onBindViewHolder,效果很明显。

    接口使用:

    1、 RecyclerView.RecycledViewPool recycledViewPool = new RecyclerView.RecycledViewPool();

    2、 recyclerView.setRecycledViewPool();

    优化三:去除默认动画

    解析:默认在开启item动画的情况下会使rv额外处理很多的逻辑判断,notify的增删改操作都会对应相应的item动画效果,所以如果你的应用不需要这些动画效果的话可以直接关闭掉,这样可以在处理增删改操作时大大简化rv的内部逻辑处理。

    接口使用:直接调用setItemAnimator(null)即可。

    优化四:设置点击事件

    解析: onBindViewHolder() 中频繁创建新的 onClickListener 实例没有必要,建议实际开发中应该在 onCreateViewHolder() 中每次为新建的 View 设置一次就行。

    代码实例:

    @NonNull

    @Override

    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

    final View view = LayoutInflater.from(mContext).inflate(R.layout.item_me_gv_grid, parent, false);

                final MyViewHolder holder = new MyViewHolder(view);

                view.setOnClickListener(new View.OnClickListener() {

                    @Override

                    public void onClick(View v) {

                        if (listener != null) {

                            listener.onItemClick(view, holder.getLayoutPosition());

                        }

                    }

                });

                return holder;

            }

    还有实现方式吗?

    使用接口:addOnItemTouchListener();

    优化五:diffUtils的使用

    解析:diffutil是配合rv进行差异化比较的工具类,通过对比前后两个data数据集合,diffutil会自动给出一系列的notify操作,避免我们手动调用notifiy的繁琐。

    使用:

    1、实现DiffUtil.Callback抽象类

    public abstract static class Callback {

            public Callback() {

            }

            public abstract int getOldListSize();

            public abstract int getNewListSize();

            public abstract boolean areItemsTheSame(int var1, int var2);

            public abstract boolean areContentsTheSame(int var1, int var2);

            @Nullable

            public Object getChangePayload(int oldItemPosition, int newItemPosition) {

                return null;

            }

        }

    2、调用代码:

    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( newDiffCallBack(), true);

    diffResult.dispatchUpdatesTo(adapter);

    adapter.setData(newData);

            最后介绍一个关于RecyclerVIew的Adapter的开源项目,BaseRecyclerViewAdapterHelper,里面就封装了DiffUtil的使用,可以查看接口setNewDiffData。

    四:相关开源框架介绍与Rv实现常见UI介绍

    项目一:BaseRecyclerViewAdapterHelper(18.9k)

    项目简介:

    1、优化Adapter代码:和原始的adapter相对,减少70%的代码量。

    2、添加Item事件:Item的点击事件,Item的长按事件,Item子控件的点击事件,Item子控件的长按事件

    3、添加列表加载动画:一行代码轻松切换5种默认动画。

    4、添加头部、尾部:一行代码搞定,感觉又回到ListView时代。

    5、自动加载:上拉加载无需监听滑动事件,可自定义加载布局,显示异常提示,自定义异常提示。同时支持下拉加载。

    6、分组布局:随心定义分组头部。

    7、多布局:简单配置、无需重写额外方法。

    8、设置空布局:比Listview的setEmptyView还要好用。

    9、添加拖拽、滑动删除:开启,监听即可,就是这么简单。

    10、树形列表:比ExpandableListView还要强大,支持多级。

    11、自定义ViewHolder:支持自定义ViewHolder,让开发者随心所欲。

    12、扩展框架:组合第三方框架,轻松实现更多需求定制。

    项目地址:https://github.com/CymChad/BaseRecyclerViewAdapterHelper

    项目二:UltimateRecyclerView(7.1k)

    项目简介:UltimateRecyclerview是一种功能强大的RecyclerView(advanced and flexible version ofListView),包括了下拉刷新,加载更多,多种动画,空数据提示,拖动排序,视差处理,工具栏渐变,滑动删除,自定义floating button,多种刷新效果,scrollbar等等元素。

    项目地址:https://github.com/cymcsg/UltimateRecyclerView

    项目三:vlayout(10k)

    项目简介:VirtualLayout是一个针对RecyclerView的LayoutManager扩展,主要提供一整套布局方案和布局间的组件复用的问题。

    设计思路:通过定制化的LayoutManager,接管整个RecyclerView的布局逻辑;LayoutManager管理了一系列LayoutHelper,LayoutHelper负责具体布局逻辑实现的地方;每一个LayoutHelper负责页面某一个范围内的组件布局;不同的LayoutHelper可以做不同的布局逻辑,因此可以在一个RecyclerView页面里提供异构的布局结构,这就能比系统自带的LinearLayoutManager、GridLayoutManager等提供更加丰富的能力。同时支持扩展LayoutHelper来提供更多的布局能力。

    项目地址:https://github.com/alibaba/vlayout

    Rv实现常见UI介绍:

    一:抖音首页实现原理

    旧方案思路:垂直的viewpager+下拉刷新上拉加载控件+ fragment的组合方案

    新方案思路:RecyclerView+SnapHelper++ fragment的组合方案

    什么是SnapHelper:

           SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper的子类,可以让RecyclerView滚动停止时相应的Item停留中间位置。在25.1.0版本中,官方又提供了一个PagerSnapHelper的子类,可以使RecyclerView像ViewPager一样的效果,一次只能滑一页,而且居中显示。

    二:ViewPager2实现原理(继承RecyclerView)

    新功能:

    1、支持RTL布局(Right To Left)

    2、支持竖向滚动

    3、完整支持notifyDataSetChanged

    API的变动:

    1、FragmentStateAdapter替换了原来的FragmentStatePagerAdapter。

    2、RecyclerView.Adapter替换了原来的PagerAdapter。

    3、registerOnPageChangeCallback替换了原来的addPageChangeListener。

    实现下滑翻页:RecyclerView+LinearLayoutManager+PagerSnapHelper

    五:扩展阅读

    1、http://www.sohu.com/a/300876569_611601(看完感觉我RecyclerView白学了!)

    2、https://juejin.im/post/5cce410551882541e40e471d(RecyclerView问题汇总)

    3、https://www.jianshu.com/p/193fb966e954(AndroidListView与RecyclerView对比浅析--缓存机制)

    4、https://www.jianshu.com/p/aedb2842de30(RecyclerView性能优化|安卓offer收割基)

    5、https://www.jianshu.com/p/45a43a117365(AndroidRecyclerView与ListView局部刷新)

    6、https://www.jianshu.com/p/122e68e9ddac(RecycledViewPool使用)

    7、https://www.jianshu.com/p/29352def27e6(RecyclerViewnotifyDataSetChanged导致图片闪烁的真凶)

    8、https://blog.csdn.net/qq_21138819/article/details/83028693(【Android进阶】仿抖音系列之翻页上下滑切换视频)

    9、https://juejin.im/post/5bb85f52f265da0af609c685(快速实现android版抖音主界面的心得)

    10、https://www.jianshu.com/p/e0bd595d6321(【Android进阶】仿抖音系列之翻页上下滑切换视频(四))

    11、https://www.jianshu.com/p/e54db232df62(让你明明白白的使用RecyclerView——SnapHelper详解)

    相关文章

      网友评论

          本文标题:ListView和RecyclerView详解

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