深入浅出 RecyclerView

2018-05-03 15:05:32 浏览数 (1)

原文:http://kymjs.com/code/2016/07/10/01 作者:kymjs张涛

今天推荐给各位的是张涛同学最近的一篇文章,说实话,RecyclerView 的文章挺多的,但像这样由浅入深,一步步讲到源码实现工作原理的不是那么多,推荐大家阅读。

起深入浅出这名字的时候我是慎重又慎重的,生怕被人骂标题党,写的什么破玩意还敢说深入浅出。所以还是请大家不要抱着太高的期望,因为没有期望就没有失望,就像陈润说的,超预期嘛。全当看小说的心情来看这系列文章了。

这篇文章分几个部分,简单跟大家讲一下 RecyclerView 的常用方法与奇葩用法;工作原理与 ListView 比较;源码解析;

常用方法

RecyclerView 与 ListView、GridView 类似,都是可以显示同一种类型 View 的集合的控件。

首先看看最简单的用法,四步走:

0.接入

build.gradle 文件中加入

代码语言:javascript复制
compile 'com.android.support:recyclerview-v7:24.0.0'
1.创建对象
代码语言:javascript复制
RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);
2.设置显示规则
代码语言:javascript复制
recyclerview.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));

RecyclerView 将所有的显示规则交给一个叫 LayoutManager 的类去完成了。

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

3.设置适配器
代码语言:javascript复制
recyclerview.setAdapter(adapter);

适配器,同 ListView 一样,用来设置每个item显示内容的。

通常,我们写 ListView 适配器,都是:

  • 首先继承 BaseAdapter
  • 实现四个抽象方法;
  • 创建一个静态 ViewHolder
  • getView() 方法中判断 convertView 是否为空,创建还是获取 ViewHolder对象。

RecyclerView 也是类似的步骤:

  • 首先继承RecyclerView.Adapter<VH>类;
  • 实现三个抽象方法;
  • 创建一个静态的 ViewHolder

不过 RecyclerViewViewHolder 创建稍微有些限制,类名就是上面继承的时候泛型中声明的类名(或者应该说,上面泛型中的类名需要是这个holder的类名);并且 ViewHolder 必须继承自RecyclerView.ViewHolder类。

代码语言:javascript复制
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 VH onCreateViewHolder(ViewGroup parent, int viewType) {
    return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));
  }

  @Override
  public void onBindViewHolder(VH holder, int position) {
    holder.mTextView.setText(dataList.get(position).getNum());
  }

  @Override
  public int getItemCount() {
    return dataList.size();
  }
  
  public static class VH extends RecyclerView.ViewHolder {
    
    TextView mTextView;
    
    public VH(View itemView) {
      mTextView = (TextView) itemView.findViewById(android.R.id.text1);
    }
  }
}

更多方法

除了常用方法,当然还有不常用的。

瀑布流与滚动方向

前面已经介绍过,RecyclerView实现瀑布流,可以通过一句话设置:recycler.setLayoutManager(new StaggeredGridLayoutManager(2, VERTICAL))就可以了。

其中 StaggeredGridLayoutManager 第一个参数表示列数,就好像 GridView的列数一样,第二个参数表示方向,可以很方便的实现横向滚动或者纵向滚动。

使用 demo 可以查看:Github 【RecyclerView简单使用】

添加删除 item 的动画

ListView 每次修改了数据源后,都要调用 notifyDataSetChanged() 刷新每项 item 类似,只不过 RecyclerView 还支持局部刷新notifyItemInserted(index)、 notifyItemRemoved(position)、 notifyItemChanged(position)。

在添加或删除了数据后,RecyclerView 还提供了一个默认的动画效果,来改变显示。同时,你也可以定制自己的动画效果:模仿 DefaultItemAnimator 或直接继承这个类,实现自己的动画效果,并调用recyclerview.setItemAnimator(new DefaultItemAnimator()); 设置上自己的动画。

使用 demo 可以查看:Github 【RecyclerView默认动画】

LayoutManager的常用方法

  • findFirstVisibleItemPosition() 返回当前第一个可见 Item 的 position
  • findFirstCompletelyVisibleItemPosition() 返回当前第一个完全可见 Item 的 position
  • findLastVisibleItemPosition() 返回当前最后一个可见 Item 的 position
  • findLastCompletelyVisibleItemPosition() 返回当前最后一个完全可见 Item 的 position.
  • scrollBy() 滚动到某个位置。

adapter封装

其实很早之前写过一篇关于 RecyclerView 适配器的封装,所以这不再赘述了,传送门:RecyclerView的通用适配器

使用 demo 可以查看:Github 【RecyclerView通用适配器演示】

吐槽

OnItemTouchListener 什么鬼?

用习惯了 ListViewOnItemClickListenerRecyclerView 你的OnItemClickListener 呢?

Tell me where do I find, something like ListView listener ?

好吧,翻遍了 API 列表,就找到了个 OnItemTouchListener ,这特么什么鬼,我干嘛要对每个 item 监听触摸屏事件。

万万没想到,最终我还是在 Google IO 里面的介绍找到了原因。原来是 Google 的工程师分不清究竟是改给 listview 的 item 添加点击事件,还是应该给每个 item 的 view 添加点击事件,索性就不给 OnItemClickListener 了,然后在 support demo 里面,你就会发现,RecyclerView 的 item 点击事件都是写在了 adapter 的 ViewHolder 里面。

当然,除了 support demo 包里面使用的在 ViewHolder 里面设置点击事件以外,我还写好了一个 RecyclerView 使用的 OnItemClickListener 代码请见:RecyclerItemClickListener.java

需要一提的是,网上有很多这种类似的 ItemClickListener ,在使用的时候一定注意一个问题,就是循环引用问题。比如 listener 里面持有了一个 recyclerview, 而这个 recyclerview 在调用 setListener() 的时候又持有了一个 listener。尽管 Java 虚拟机现在可以解决这种问题了,但作为代码编写者,这种写法还是应该尽量避免的。

divider 跑哪了?

ListView中设置 divider 非常简单,只需要在 XML 文件中设置就可以了,同时还可以设置 divider 高度。

代码语言:javascript复制
android:divider="@android:color/black"
android:dividerHeight="2dp"

而在RecyclerView里面,想实现这两种需求,稍微复杂一点,需要自己继承RecyclerView.ItemDecoration来实现想要实现的方法。

虽说这样写灵活多了,但是要额外写一个类去做难免麻烦,这里大家可以看我已经实现好的一个封装,包括显示纯色divider显示图片dividerdivider的上下左右的间距宽高设置 应该可以满足基本需求了:Divider.java

使用 demo 可以查看:Github 【自定义 Divider 使用】

五虎上将工作原理

借用 Google IO 视频中的一张截图: 视频的完整地址可查看: RecyclerView ins and outs - Google I/O 2016

其实上图中并没有写完整,大 boss RecyclerView 应该有这五虎上将:

LayoutManager工作原理

代码语言:javascript复制
java.lang.Object
   ↳ android.view.View
        ↳ android.view.ViewGroup
            ↳ android.support.v7.widget.RecyclerView

首先是 RecyclerView 继承关系,可以看到,与 ListView 不同,他是一个 ViewGroup。既然是一个 View,那么就不可少的要经历onMeasure()onLayout()onDraw() 这三个方法。

实际上,RecyclerView 就是将 onMeasure()onLayout() 交给了 LayoutManager 去处理,因此如果给 RecyclerView 设置不同的 LayoutManager 就可以达到不同的显示效果,因为onMeasure()onLayout()都不同了嘛。

ItemDecoration 工作原理

ItemDecoration 是为了显示每个 item 之间分隔样式的。它的本质实际上就是一个 Drawable。当 RecyclerView 执行到 onDraw() 方法的时候,就会调用到他的onDraw(),这时,如果你重写了这个方法,就相当于是直接在 RecyclerView 上画了一个 Drawable 表现的东西。

而最后,在他的内部还有一个叫getItemOffsets()的方法,从字面就可以理解,他是用来偏移每个 item 视图的。当我们在每个 item 视图之间强行插入绘画了一段 Drawable,那么如果再照着原本的逻辑去绘 item 视图,就会覆盖掉 Decoration 了,所以需要getItemOffsets()这个方法,让每个 item 往后面偏移一点,不要覆盖到之前画上的分隔样式了。

ItemAnimator

每一个 item 在特定情况下都会执行的动画。说是特定情况,其实就是在视图发生改变,我们手动调用notifyxxxx()的时候。通常这个时候我们会要传一个下标,那么从这个标记开始一直到结束,所有 item 视图都会被执行一次这个动画。

Adapter工作原理

首先是适配器,适配器的作用都是类似的,用于提供每个 item 视图,并返回给RecyclerView 作为其子布局添加到内部。

但是,与 ListView 不同的是,ListView 的适配器是直接返回一个 View,将这个 View 加入到 ListView 内部。而 RecyclerView 是返回一个 ViewHolder 并且不是直接将这个 holder 加入到视图内部,而是加入到一个缓存区域,在视图需要的时候去缓存区域找到 holder 再间接的找到 holder 包裹的 View。

ViewHolder

每个 ViewHolder 的内部是一个 View,并且 ViewHolder 必须继承自RecyclerView.ViewHolder类。 这主要是因为 RecyclerView 内部的缓存结构并不是像 ListView 那样去缓存一个 View,而是直接缓存一个 ViewHolder ,在 ViewHolder 的内部又持有了一个 View。既然是缓存一个 ViewHolder,那么当然就必须所有的 ViewHolder 都继承同一个类才能做到了。

缓存与复用的原理

还是一张截图

RecyclerView 的内部维护了一个二级缓存,滑出界面的 ViewHolder 会暂时放到 cache 结构中,而从 cache 结构中移除的 ViewHolder,则会放到一个叫做RecycledViewPool 的循环缓存池中。

顺带一说,RecycledView 的性能并不比 ListView 要好多少,它最大的优势在于其扩展性。但是有一点,在 RecycledView 内部的这个第二级缓存池RecycledViewPool 是可以被多个 RecyclerView 共用的,这一点比起直接缓存 View 的 ListView 就要高明了很多,但也正是因为需要被多个 RecyclerView 公用,所以我们的 ViewHolder 必须继承自同一个基类(即RecyclerView.ViewHolder)。

默认的情况下,cache 缓存 2 个 holder,RecycledViewPool 缓存 5 个 holder。对于二级缓存池中的 holder 对象,会根据 viewType 进行分类,不同类型的 viewType 之间互不影响。

Demo & PPT

写了这么多累死我了,就这样吧,最后发一个 demo 地址:RecyclerViewDemo

和一份内部分享的 PPT 地址:RecyclerView PPT

0 人点赞