乐趣区

RecyclerView问题汇总

目录介绍

  • 25.0.0.0 请说一下 RecyclerView?adapter 的作用是什么,几个方法是做什么用的?如何理解 adapter 订阅者模式?
  • 25.0.0.1 ViewHolder 的作用是什么?如何理解 ViewHolder 的复用?什么时候停止调用 onCreateViewHolder?
  • 25.0.0.2 ViewHolder 封装如何对 findViewById 优化?ViewHolder 中为何使用 SparseArray 替代 HashMap 存储 viewId?
  • 25.0.0.3 LayoutManager 作用是什么?LayoutManager 样式有哪些?setLayoutManager 源码里做了什么?
  • 25.0.0.4 SnapHelper 主要是做什么用的?SnapHelper 是怎么实现支持 RecyclerView 的对齐方式?
  • 25.0.0.5 SpanSizeLookup 的作用是干什么的?SpanSizeLookup 如何使用?SpanSizeLookup 实现原理如何理解?
  • 25.0.0.6 ItemDecoration 的用途是什么?自定义 ItemDecoration 有哪些重写方法?分析一下 addItemDecoration()源码?
  • 25.0.0.7 上拉加载更多的功能是如何做的?添加滚动监听事件需要注意什么问题?网格布局上拉加载如何优化?
  • 25.0.0.8 RecyclerView 绘制原理如何理解?性能优化本质是什么?RecyclerView 绘制原理过程大概是怎样的?
  • 25.0.0.9 RecyclerView 的 Recyler 是如何实现 ViewHolder 的缓存?如何理解 recyclerView 三级缓存是如何实现的?
  • 25.0.1.0 屏幕滑动 (状态是 item 状态可见,不可见,即将可见变化) 时三级缓存是如何理解的?adapter 中的几个方法是如何变化?
  • 25.0.1.1 SnapHelper 有哪些重要的方法,其作用就是是什么?LinearSnapHelper 中是如何实现滚动停止的?
  • 25.0.1.2 LinearSnapHelper 代码中 calculateDistanceToFinalSnap 作用是什么?那么 out[0]和 out[1]分别指什么?
  • 25.0.1.3 如何实现可以设置分割线的颜色,宽度,以及到左右两边的宽度间距的自定义分割线,说一下思路?
  • 25.0.1.4 如何实现复杂 type 首页需求?如果不封装会出现什么问题和弊端?如何提高代码的简便性和高效性?
  • 25.0.1.5 关于 item 条目点击事件在 onCreateViewHolder 中写和在 onBindViewHolder 中写有何区别?如何优化?
  • 25.0.1.6 RecyclerView 滑动卡顿原因有哪些?如何解决嵌套布局滑动冲突?如何解决 RecyclerView 实现画廊卡顿?
  • 25.0.1.7 RecyclerView 常见的优化有哪些?实际开发中都是怎么做的,优化前后对比性能上有何提升?
  • 25.0.1.8 如何解决 RecyclerView 嵌套 RecyclerView 条目自动上滚的 Bug?如何解决 ScrollView 嵌套 RecyclerView 滑动冲突?
  • 25.0.1.9 如何处理 ViewPager 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager?如何解决 RecyclerView 使用 Glide 加载图片导致图片错乱问题?
  • 00.RecyclerView 复杂封装库

    • 几乎融合了该系列博客中绝大部分的知识点,欢迎一遍看博客一遍实践,一步步从简单实现功能强大的库
  • 01.RecyclerView

    • RecycleView 的结构,RecyclerView 简单用法介绍
  • 02.Adapter

    • RecyclerView.Adapter 扮演的角色,一般常用的重写方法说明,数据变更通知之观察者模式,查看.notifyChanged(); 源码
  • 03.ViewHolder

    • ViewHolder 的作用,如何理解对于 ViewHolder 对象的数量“够用”之后就停止调用 onCreateViewHolder 方法,ViewHolder 简单封装
  • 04.LayoutManager

    • LayoutManager 作用是什么?setLayoutManager 源码分析
  • 05.SnapHelper

    • SnapHelper 作用,什么是 Fling 操作,SnapHelper 类重要的方法,
  • 06.ItemTouchHelper
  • 07.SpanSizeLookup

    • SpanSizeLookup 如何使用,同时包含列表,2 列的网格,3 列的网格如何优雅实现?
  • 08.ItemDecoration

    • ItemDecoration 的用途,addItemDecoration()源码分析
  • 09.RecycledViewPool

    • RecyclerViewPool 用于多个 RecyclerView 之间共享 View。
  • 10.ItemAnimator

    • 官方有一个默认 Item 动画类 DafaultItemAnimator, 其中 DefaultItemAnimator 继承了 SimpleItemAnimator, 在继承了 RecyclerView.ItemAnimator,它是如何实现动画呢?
  • 11.RecyclerView 上拉加载

    • 添加 recyclerView 的滑动事件,上拉加载分页数据,设置上拉加载的底部 footer 布局,显示和隐藏 footer 布局
  • 12.RecyclerView 缓存原理

    • RecyclerView 做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载,复用池,刷新数据等等
  • 13.SnapHelper 源码分析

    • SnapHelper 旨在支持 RecyclerView 的对齐方式,也就是通过计算对齐 RecyclerView 中 TargetView 的指定点或者容器中的任何像素点。
  • 16. 自定义 SnapHelper

    • 自定义 SnapHelper
  • 18.ItemTouchHelper 实现交互动画

    • 需要自定义类实现 ItemTouchHelper.Callback 类
  • 19. 自定义 ItemDecoration 分割线

    • 需要自定义类实现 RecyclerView.ItemDecoration 类,并选择重写合适方法
  • 21.RecyclerView 优化处理

    • RecyclerView 滑动卡顿原因有哪些?如何解决嵌套布局滑动冲突?如何解决 RecyclerView 实现画廊卡顿?
  • 22.RecyclerView 问题汇总

    • getLayoutPosition()和 getAdapterPosition()的区别
  • 23.RecyclerView 滑动冲突

    • 01. 如何判断 RecyclerView 控件滑动到顶部和底部
    • 02.RecyclerView 嵌套 RecyclerView 条目自动上滚的 Bug
    • 03.ScrollView 嵌套 RecyclerView 滑动冲突
    • 04.ViewPager 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager
    • 05.RecyclerView 嵌套 RecyclerView 的滑动冲突问题
    • 06.RecyclerView 使用 Glide 加载图片导致图片错乱问题解决
  • 24.ScrollView 嵌套 RecyclerView 问题

    • 要实现在 NestedScrollView 中嵌入一个或多个 RecyclerView,会出现滑动冲突,焦点抢占,显示不全等。如何处理?
  • 25.RecyclerView 封装库和综合案例

    • 自定义支持上拉加载更多【加载中,加载失败 [比如没有更多数据],加载异常[无网络],加载成功等多种状态】,下拉刷新,可以实现复杂的状态页面,支持自由切换状态【加载中,加载成功,加载失败,没网络等状态】的控件,拓展功能[支持长按拖拽,侧滑删除] 可以选择性添加。具体使用方法,可以直接参考 demo 案例。

25.0.0.0 请说一下 RecyclerView?adapter 的作用是什么,几个方法是做什么用的?如何理解 adapter 订阅者模式?

  • 关于 RecyclerView,大家都已经很熟悉了,用途十分广泛,大概结构如下所示

    • RecyclerView.Adapter – 处理数据集合并负责绑定视图
    • ViewHolder – 持有所有的用于绑定数据或者需要操作的 View
    • LayoutManager – 负责摆放视图等相关操作
    • ItemDecoration – 负责绘制 Item 附近的分割线
    • ItemAnimator – 为 Item 的一般操作添加动画效果,如,增删条目等
  • 如图所示,直观展示结构

  • adapter 的作用是什么

    • RecyclerView.Adapter 扮演的角色
    • 一是,根据不同 ViewType 创建与之相应的的 Item-Layout
    • 二是,访问数据集合并将数据绑定到正确的 View 上
  • 几个方法是做什么用的

    • 一般常用的重写方法有以下这么几个:博客
    public VH onCreateViewHolder(ViewGroup parent, int viewType)
    创建 Item 视图,并返回相应的 ViewHolder
    public void onBindViewHolder(VH holder, int position)
    绑定数据到正确的 Item 视图上。public int getItemCount()
    返回该 Adapter 所持有的 Itme 数量
    public int getItemViewType(int position)
    用来获取当前项 Item(position 参数)是哪种类型的布局
  • 如何理解 adapter 订阅者模式

    • 当时据集合发生改变时,我们通过调用.notifyDataSetChanged(),来刷新列表,因为这样做会触发列表的重绘。
    • 注意这里需要理解什么是订阅者模式……
    • a. 首先看.notifyDataSetChanged()源码

      public final void notifyDataSetChanged() {mObservable.notifyChanged();
      }
    • b. 接着查看.notifyChanged(); 源码

      • 被观察者 AdapterDataObservable,内部持有观察者 AdapterDataObserver 集合
      static class AdapterDataObservable extends Observable<AdapterDataObserver> {public boolean hasObservers() {return !mObservers.isEmpty();
          }
      
          public void notifyChanged() {for (int i = mObservers.size() - 1; i >= 0; i--) {mObservers.get(i).onChanged();}
          }
      
          public void notifyItemRangeChanged(int positionStart, int itemCount) {notifyItemRangeChanged(positionStart, itemCount, null);
          }
      
          public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {for (int i = mObservers.size() - 1; i >= 0; i--) {mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
              }
          }
      
          public void notifyItemRangeInserted(int positionStart, int itemCount) {for (int i = mObservers.size() - 1; i >= 0; i--) {mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
              }
          }
      }
      • 观察者 AdapterDataObserver,具体实现为 RecyclerViewDataObserver,当数据源发生变更时,及时响应界面变化
      public static abstract class AdapterDataObserver {public void onChanged() {// Do nothing}
      
          public void onItemRangeChanged(int positionStart, int itemCount) {// do nothing}
      
          public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {onItemRangeChanged(positionStart, itemCount);
          }
      }
    • c. 接着查看 setAdapter()源码中的 setAdapterInternal(adapter, false, true)方法

      • setAdapter 源码博客
      public void setAdapter(Adapter adapter) {
          // bail out if layout is frozen
          setLayoutFrozen(false);
          setAdapterInternal(adapter, false, true);
          requestLayout();}
      • setAdapterInternal(adapter, false, true)源码
      private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
              boolean removeAndRecycleViews) {if (mAdapter != null) {mAdapter.unregisterAdapterDataObserver(mObserver);
              mAdapter.onDetachedFromRecyclerView(this);
          }
          if (!compatibleWithPrevious || removeAndRecycleViews) {removeAndRecycleViews();
          }
          mAdapterHelper.reset();
          final Adapter oldAdapter = mAdapter;
          mAdapter = adapter;
          if (adapter != null) {
              // 注册一个观察者 RecyclerViewDataObserver
              adapter.registerAdapterDataObserver(mObserver);
              adapter.onAttachedToRecyclerView(this);
          }
          if (mLayout != null) {mLayout.onAdapterChanged(oldAdapter, mAdapter);
          }
          mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
          mState.mStructureChanged = true;
          markKnownViewsInvalid();}
    • d.notify……方法被调用,刷新数据

      • 当数据变更时,调用 notify** 方法时,Adapter 内部的被观察者会遍历通知已经注册的观察者的对应方法,这时界面就会响应变更。博客

25.0.0.1 ViewHolder 的作用是什么?如何理解 ViewHolder 的复用?什么时候停止调用 onCreateViewHolder?

  • ViewHolder 作用大概有这些:

    • adapter 应当拥有 ViewHolder 的子类,并且 ViewHolder 内部应当存储一些子 view,避免时间代价很大的 findViewById 操作
    • 其 RecyclerView 内部定义的 ViewHolder 类包含很多复杂的属性,内部使用场景也有很多,而我们经常使用的也就是 onCreateViewHolder()方法和 onBindViewHolder()方法,onCreateViewHolder()方法在 RecyclerView 需要一个新类型。item 的 ViewHolder 时调用来创建一个 ViewHolder,而 onBindViewHolder()方法则当 RecyclerView 需要在特定位置的 item 展示数据时调用。博客
  • 如何理解 ViewHolder 的复用

    • 在复写 RecyclerView.Adapter 的时候,需要我们复写两个方法:博客

      • onCreateViewHolder
      • onBindViewHolder
      • 这两个方法从字面上看就是创建 ViewHolder 和绑定 ViewHolder 的意思
    • 复用机制是怎样的?

      • 模拟场景:只有一种 ViewType,上下滑动的时候需要的 ViewHolder 种类是只有一种,但是需要的 ViewHolder 对象数量并不止一个。所以在后面创建了 9 个 ViewHolder 之后,需要的数量够了,无论怎么滑动,都只需要复用以前创建的对象就行了。那么逗比程序员们思考一下,为什么会出现这种情况呢
      • 看到了下面 log 之后,第一反应是在这个 ViewHolder 对象的数量“够用”之后就停止调用 onCreateViewHolder 方法,但是 onBindViewHolder 方法每次都会调用的
    • 查看一下 createViewHolder 源代码

      • 发现这里并没有限制
      public final VH createViewHolder(ViewGroup parent, int viewType) {TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
          final VH holder = onCreateViewHolder(parent, viewType);
          holder.mItemViewType = viewType;
          TraceCompat.endSection();
          return holder;
      }
  • 对于 ViewHolder 对象的数量“够用”之后就停止调用 onCreateViewHolder 方法,可以查看

    • 获取为给定位置初始化的视图。博客
    • 此方法应由 {@link LayoutManager} 实现使用,以获取视图来表示来自 {@LinkAdapter} 的数据。
    • 如果共享池可用于正确的视图类型,则回收程序可以重用共享池中的废视图或分离视图。如果适配器没有指示给定位置上的数据已更改,则回收程序将尝试发回一个以前为该数据初始化的报废视图,而不进行重新绑定。
    public View getViewForPosition(int position) {return getViewForPosition(position, false);
    }
    
    View getViewForPosition(int position, boolean dryRun) {return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }
    
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {// 代码省略了,有需要的小伙伴可以自己看看,这里面逻辑实在太复杂呢}
25.0.0.2 ViewHolder 封装如何对 findViewById 优化?ViewHolder 中为何使用 SparseArray 替代 HashMap 存储 viewId?
  • ViewHolder 封装如何对 findViewById 优化?

    class MyViewHolder extends RecyclerView.ViewHolder {
    
        private SparseArray<View> viewSparseArray;
        private TextView tvTitle;
    
        MyViewHolder(final View itemView) {super(itemView);
            if(viewSparseArray==null){viewSparseArray = new SparseArray<>();
            }
            tvTitle = (TextView) viewSparseArray.get(R.id.tv_title);
            if (tvTitle == null) {tvTitle = itemView.findViewById(R.id.tv_title);
                viewSparseArray.put(R.id.tv_title, tvTitle);
            }
        }
    }
  • 为何使用 SparseArray 替代 HashMap 存储 viewId

    • HashMap

      • 基本上就是一个 HashMap.Entry 的数组(Entry 是 HashMap 的一个内部类)。更准确来说,Entry 类中包含以下字段:
      • 一个非基本数据类型的 key
      • 一个非基本数据类型的 value
      • 保存对象的哈希值
      • 指向下一个 Entry 的指针
    • 当有键值对插入时,HashMap 会发生什么 ?

      • 首先,键的哈希值被计算出来,然后这个值会赋给 Entry 类中对应的 hashCode 变量。
      • 然后,使用这个哈希值找到它将要被存入的数组中“桶”的索引。
      • 如果该位置的“桶”中已经有一个元素,那么新的元素会被插入到“桶”的头部,next 指向上一个元素——本质上使“桶”形成链表。
    • 现在,当你用 key 去查询值时,时间复杂度是 O(1)。虽然时间上 HashMap 更快,但同时它也花费了更多的内存空间。
    • 缺点:

      • 自动装箱的存在意味着每一次插入都会有额外的对象创建。这跟垃圾回收机制一样也会影响到内存的利用。
      • HashMap.Entry 对象本身是一层额外需要被创建以及被垃圾回收的对象。
      • “桶”在 HashMap 每次被压缩或扩容的时候都会被重新安排。这个操作会随着对象数量的增长而变得开销极大
      • 在 Android 中,当涉及到快速响应的应用时,内存至关重要,因为持续地分发和释放内存会出发垃圾回收机制,这会拖慢应用运行。垃圾回收机制会影响应用性能表现,垃圾回收时间段内,应用程序是不会运行的,最终应用使用上就显得卡顿。
    • SparseArray博客

      • 它里面也用了两个数组。一个 int[] mKeys 和 Object[] mValues。从名字都可以看得出来一个用来存储 key 一个用来保存 value 的。
    • 当保存一对键值对的时候:

      • key(不是它的 hashcode)保存在 mKeys[]的下一个可用的位置上。所以不会再对 key 自动装箱了。
      • value 保存在 mValues[]的下一个位置上,value 还是要自动装箱的,如果它是基本类型。
    • 查找的时候:

      • 查找 key 还是用的二分法查找。也就是说它的时间复杂度还是 O(logN)
      • 知道了 key 的 index,也就可以用 key 的 index 来从 mValues 中检索出 value。
    • 相较于 HashMap, 我们舍弃了 Entry 和 Object 类型的 key, 放弃了 HashCode 并依赖于二分法查找。在添加和删除操作的时候有更好的性能开销。

25.0.0.3 LayoutManager 作用是什么?LayoutManager 样式有哪些?setLayoutManager 源码里做了什么?

  • LayoutManager 作用是什么?

    • LayoutManager 的职责是摆放 Item 的位置,并且负责决定何时回收和重用 Item。博客
    • RecyclerView 允许自定义规则去放置子 view,这个规则的控制者就是 LayoutManager。一个 RecyclerView 如果想展示内容,就必须设置一个 LayoutManager
  • LayoutManager 样式有哪些?

    • LinearLayoutManager 水平或者垂直的 Item 视图。
    • GridLayoutManager 网格 Item 视图。
    • StaggeredGridLayoutManager 交错的网格 Item 视图。
  • setLayoutManager(LayoutManager layout)源码

    • 分析:当之前设置过 LayoutManager 时,移除之前的视图,并缓存视图在 Recycler 中,将新的 mLayout 对象与 RecyclerView 绑定,更新缓存 View 的数量。最后去调用 requestLayout,重新请求 measure、layout、draw。
    public void setLayoutManager(LayoutManager layout) {if (layout == mLayout) {return;}
        // 停止滑动
        stopScroll();
        if (mLayout != null) {
            // 如果有动画,则停止所有的动画
            if (mItemAnimator != null) {mItemAnimator.endAnimations();
            }
            // 移除并回收视图
            mLayout.removeAndRecycleAllViews(mRecycler);
            // 回收废弃视图
            mLayout.removeAndRecycleScrapInt(mRecycler);
            // 清除 mRecycler
            mRecycler.clear();
            if (mIsAttached) {mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {mRecycler.clear();
        }
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager" + layout +
                        "is already attached to a RecyclerView:" + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {mLayout.dispatchAttachedToWindow(this);
            }
        }
        // 更新新的缓存数据
        mRecycler.updateViewCacheSize();
        // 重新请求 View 的测量、布局、绘制
        requestLayout();}

25.0.0.4 SnapHelper 主要是做什么用的?SnapHelper 是怎么实现支持 RecyclerView 的对齐方式?

  • SnapHelper 主要是做什么用的

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

    • SnapHelper 旨在支持 RecyclerView 的对齐方式,也就是通过计算对齐 RecyclerView 中 TargetView 的指定点或者容器中的任何像素点。博客
  • SnapHelper 类重要的方法

    • attachToRecyclerView: 将 SnapHelper attach 到指定的 RecyclerView 上。
    • calculateDistanceToFinalSnap: 复写这个方法计算对齐到 TargetView 或容器指定点的距离,这是一个抽象方法,由子类自己实现,返回的是一个长度为 2 的 int 数组 out,out[0]是 x 方向对齐要移动的距离,out[1]是 y 方向对齐要移动的距离。
    • calculateScrollDistance: 根据每个方向给定的速度估算滑动的距离,用于 Fling 操作。
    • findSnapView: 提供一个指定的目标 View 来对齐, 抽象方法,需要子类实现
    • findTargetSnapPosition: 提供一个用于对齐的 Adapter 目标 position, 抽象方法,需要子类自己实现。
    • onFling: 根据给定的 x 和 y 轴上的速度处理 Fling。
  • 什么是 Fling 操作

    • 手指在屏幕上滑动 RecyclerView 然后松手,RecyclerView 中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做 Fling。Fling 操作从手指离开屏幕瞬间被触发,在滚动停止时结束。
  • LinearSnapHelper 类分析

    • LinearSnapHelper 使当前 Item 居中显示,常用场景是横向的 RecyclerView,类似 ViewPager 效果,但是又可以快速滑动(滑动多页)。博客
    • 最简单的使用就是,如下代码

      • 几行代码就可以用 RecyclerView 实现一个类似 ViewPager 的效果,并且效果还不错。可以快速滑动多页,当前页剧中显示,并且显示前一页和后一页的部分。
      LinearSnapHelper snapHelper = new LinearSnapHelper();
      snapHelper.attachToRecyclerView(mRecyclerView);
  • PagerSnapHelper 类分析

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

      PagerSnapHelper snapHelper = new PagerSnapHelper();
      snapHelper.attachToRecyclerView(mRecyclerView);

25.0.0.5 SpanSizeLookup 的作用是干什么的?SpanSizeLookup 如何使用?SpanSizeLookup 实现原理如何理解?

  • SpanSizeLookup 的作用是干什么的?

    • RecyclerView 可以通过 GridLayoutManager 实现网格布局,但是很少有人知道 GridLayoutManager 还可以用来设置网格中指定 Item 的列数,类似于合并单元格的功能,而所有的这些我们仅仅只需通过定义一个 RecycleView 列表就可以完成,要实现指定某个 item 所占列数的功能我们需要用到 GridLayoutManager.SpanSizeLookup 这个类,该类是一个抽象类,里面包含了一个 getSpanSize(int position)的抽象方法,该方法的返回值就是指定 position 所占的列数
  • SpanSizeLookup 如何使用?

    • 先是定义了一个 6 列的网格布局,然后通过 GridLayoutManager.SpanSizeLookup 这个类来动态的指定某个 item 应该占多少列。博客
    • 比如 getSpanSize 返回 6,就表示当前 position 索引处的 item 占用 6 列,那么显示就只会展示一个 ItemView【占用 6 列】。
    • 比如 getSpanSize 返回 3,就表示当前 position 索引处的 item 占用 3 列
    GridLayoutManager manager = new GridLayoutManager(this, 6);
    manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {SpanModel model = mDataList.get(position);
            if (model.getType() == 1) {return 6;} else if(model.getType() == 2){return 3;}else if (model.getType() == 3){return 2;}else if (model.getType() == 4){return 2;} else {return 1;}
        }
    });

25.0.0.6 ItemDecoration 的用途是什么?自定义 ItemDecoration 有哪些重写方法?分析一下 addItemDecoration()源码?

  • ItemDecoration 的用途是什么?

    • 通过设置 recyclerView.addItemDecoration(new DividerDecoration(this)); 来改变 Item 之间的偏移量或者对 Item 进行装饰。
    • 当然,你也可以对 RecyclerView 设置多个 ItemDecoration,列表展示的时候会遍历所有的 ItemDecoration 并调用里面的绘制方法,对 Item 进行装饰。博客
  • 自定义 ItemDecoration 有哪些重写方法

    • 该抽象类常见的方法如下所示:博客
    public void onDraw(Canvas c, RecyclerView parent)
    装饰的绘制在 Item 条目绘制之前调用,所以这有可能被 Item 的内容所遮挡
    public void onDrawOver(Canvas c, RecyclerView parent)
    装饰的绘制在 Item 条目绘制之后调用,因此装饰将浮于 Item 之上
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
    与 padding 或 margin 类似,LayoutManager 在测量阶段会调用该方法,计算出每一个 Item 的正确尺寸并设置偏移量。
  • 分析一下 addItemDecoration()源码?

    • a. 通过下面代码可知,mItemDecorations 是一个 ArrayList,我们将 ItemDecoration 也就是分割线对象,添加到其中。

      • 可以看到,当通过这个方法添加分割线后,会指定添加分割线在集合中的索引,然后再重新请求 View 的测量、布局、(绘制)。注意:requestLayout 会调用 onMeasure 和 onLayout,不一定调用 onDraw!
      • 关于 View 自定义控件源码分析,可以参考我的其他博客:https://github.com/yangchong2…
      public void addItemDecoration(ItemDecoration decor) {addItemDecoration(decor, -1);
      }
      
      // 主要看这个方法,我的 GitHub:https://github.com/yangchong211/YCBlogs
      public void addItemDecoration(ItemDecoration decor, int index) {if (mLayout != null) {
              mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                      + "layout");
          }
          if (mItemDecorations.isEmpty()) {setWillNotDraw(false);
          }
          if (index < 0) {mItemDecorations.add(decor);
          } else {
              // 指定添加分割线在集合中的索引
              mItemDecorations.add(index, decor);
          }
          markItemDecorInsetsDirty();
          // 重新请求 View 的测量、布局、绘制
          requestLayout();}
      • 总结概括博客

        • 可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。
        • 这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。
        • 所以绘制顺序就是 Decoration 的 onDraw,ItemView 的 onDraw,Decoration 的 onDrawOver。

25.0.0.7 上拉加载更多的功能是如何做的?添加滚动监听事件需要注意什么问题?网格布局上拉加载如何优化?

  • 上拉加载更多的功能是如何做的?

    • 01. 添加 recyclerView 的滑动事件

      • 首先给 recyclerView 添加滑动监听事件。那么我们知道,上拉加载时,需要具备两个条件。第一个是监听滑动到最后一个 item,第二个是滑动到最后一个并且是向上滑动。
      • 设置滑动监听器,RecyclerView 自带的 ScrollListener,获取最后一个完全显示的 itemPosition,然后判断是否滑动到了最后一个 item,
    • 02. 上拉加载分页数据

      • 然后开始调用更新上拉加载更多数据的方法。注意这里的刷新数据,可以直接用 notifyItemRangeInserted 方法,不要用 notifyDataSetChanged 方法。
    • 03. 设置上拉加载的底部 footer 布局

      • 在 adapter 中,可以上拉加载时处理 footerView 的逻辑

        • 在 getItemViewType 方法中设置最后一个 Item 为 FooterView
        • 在 onCreateViewHolder 方法中根据 viewType 来加载不同的布局
        • 最后在 onBindViewHolder 方法中设置一下加载的状态显示就可以
        • 由于多了一个 FooterView,所以要记得在 getItemCount 方法的返回值中加上 1。
    • 04. 显示和隐藏 footer 布局

      • 一般情况下,滑动底部最后一个 item,然后显示 footer 上拉加载布局,然后让其加载 500 毫秒,最后加载出下一页数据后再隐藏起来。博客
  • 网格布局上拉加载如何优化

    • 如果是网格布局,那么上拉刷新的 view 则不是居中显示,到加载更多的进度条显示在了一个 Item 上,如果想要正常显示的话,进度条需要横跨两个 Item,这该怎么办呢?
    • 在 adapter 中的 onAttachedToRecyclerView 方法中处理网格布局情况,代码如下所示,主要逻辑是如果当前是 footer 的位置,那么该 item 占据 2 个单元格,正常情况下占据 1 个单元格。
    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {super.onAttachedToRecyclerView(recyclerView);
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof GridLayoutManager) {final GridLayoutManager gridManager = ((GridLayoutManager) manager);
            gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    // 如果当前是 footer 的位置,那么该 item 占据 2 个单元格,正常情况下占据 1 个单元格
                    return getItemViewType(position) == footType ? gridManager.getSpanCount() : 1;}
            });
        }
    }
  • 那么如何实现自动进行上拉刷新?

    • 设置滑动监听,判断是否滑动到底部,也就是最后一条数据,当滑动到最后时就开始加载下一页数据,并且显示加载下一页 loading。当加载数据成功后,则直接隐藏该布局。
  • 那么如何实现手动上拉刷新呢?

    • 在上面步骤的基础上进行修改,当滑动到最后一个数据时,展示上拉加载更多布局。然后设置它的点击事件,点击之后开始加载下一页数据,当加载完成后,则直接隐藏该布局。

25.0.0.8 RecyclerView 绘制原理如何理解?性能优化本质是什么?RecyclerView 绘制原理过程大概是怎样的?

  • RecyclerView 绘制原理如何理解?

  • 性能优化本质是什么?

    • RecyclerView 做性能优化要说复杂也复杂,比如说布局优化,缓存,预加载,复用池,刷新数据等等。

      • 其优化的点很多,在这些看似独立的点之间,其实存在一个枢纽:Adapter。因为所有的 ViewHolder 的创建和内容的绑定都需要经过 Adapter 的两个函数 onCreateViewHolder 和 onBindViewHolder。
    • 因此性能优化的本质就是要减少这两个函数的调用时间和调用的次数。博客

      • 如果我们想对 RecyclerView 做性能优化,必须清楚的了解到我们的每一步操作背后,onCreateViewHolder 和 onBindViewHolder 调用了多少次。
  • RecyclerView 绘制原理过程大概是怎样的?

    • 简化问题

      RecyclerView
          以 LinearLayoutManager 为例
          忽略 ItemDecoration
          忽略 ItemAnimator
          忽略 Measure 过程
          假设 RecyclerView 的 width 和 height 是确定的
      Recycler
          忽略 mViewCacheExtension
    • 绘制过程

      • 类的职责介绍

        • LayoutManager:接管 RecyclerView 的 Measure,Layout,Draw 的过程
        • Recycler:缓存池
        • Adapter:ViewHolder 的生成器和内容绑定器。博客
      • 绘制过程简介

        • RecyclerView.requestLayout 开始发生绘制,忽略 Measure 的过程
        • 在 Layout 的过程会通过 LayoutManager.fill 去将 RecyclerView 填满
        • LayoutManager.fill 会调用 LayoutManager.layoutChunk 去生成一个具体的 ViewHolder
        • 然后 LayoutManager 就会调用 Recycler.getViewForPosition 向 Recycler 去要 ViewHolder
        • Recycler 首先去一级缓存(Cache)里面查找是否命中,如果命中直接返回。如果一级缓存没有找到,则去三级缓存查找,如果三级缓存找到了则调用 Adapter.bindViewHolder 来绑定内容,然后返回。如果三级缓存没有找到,那么就通过 Adapter.createViewHolder 创建一个 ViewHolder,然后调用 Adapter.bindViewHolder 绑定其内容,然后返回为 Recycler。
        • 一直重复步骤 3 -5,知道创建的 ViewHolder 填满了整个 RecyclerView 为止。

25.0.0.9 RecyclerView 的 Recyler 是如何实现 ViewHolder 的缓存?如何理解 recyclerView 三级缓存是如何实现的?

  • RecyclerView 的 Recyler 是如何实现 ViewHolder 的缓存?

    • 首先看看代码

      public final class Recycler {final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
          ArrayList<ViewHolder> mChangedScrap = null;
          final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
          private final List<ViewHolder>
                  mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
          private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
          int mViewCacheMax = DEFAULT_CACHE_SIZE;
          RecycledViewPool mRecyclerPool;
          private ViewCacheExtension mViewCacheExtension;
          static final int DEFAULT_CACHE_SIZE = 2;
      }
    • RecyclerView 在 Recyler 里面实现 ViewHolder 的缓存,Recycler 里面的实现缓存的主要包含以下 5 个对象:

      • ArrayList mAttachedScrap:未与 RecyclerView 分离的 ViewHolder 列表, 如果仍依赖于 RecyclerView(比如已经滑动出可视范围,但还没有被移除掉),但已经被标记移除的 ItemView 集合会被添加到 mAttachedScrap 中

        • 按照 id 和 position 来查找 ViewHolder
      • ArrayList mChangedScrap:表示数据已经改变的 viewHolder 列表, 存储 notifXXX 方法时需要改变的 ViewHolder, 匹配机制按照 position 和 id 进行匹配
      • ArrayList mCachedViews:缓存 ViewHolder,主要用于解决 RecyclerView 滑动抖动时的情况,还有用于保存 Prefetch 的 ViewHoder

        • 最大的数量为:mViewCacheMax = mRequestedCacheMax + extraCache(extraCache 是由 prefetch 的时候计算出来的)
      • ViewCacheExtension mViewCacheExtension:开发者可自定义的一层缓存,是虚拟类 ViewCacheExtension 的一个实例,开发者可实现方法 getViewForPositionAndType(Recycler recycler, int position, int type)来实现自己的缓存。

        • 位置固定
        • 内容不变
        • 数量有限
      • mRecyclerPool ViewHolder 缓存池,在有限的 mCachedViews 中如果存不下 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。

        • 按照 Type 来查找 ViewHolder
        • 每个 Type 默认最多缓存 5 个博客
  • 如何理解 recyclerView 三级缓存是如何实现的?

    • RecyclerView 在设计的时候讲上述 5 个缓存对象分为了 3 级。每次创建 ViewHolder 的时候,会按照优先级依次查询缓存创建 ViewHolder。每次讲 ViewHolder 缓存到 Recycler 缓存的时候,也会按照优先级依次缓存进去。三级缓存分别是:
    • 一级缓存:返回布局和内容都都有效的 ViewHolder

      • 按照 position 或者 id 进行匹配
      • 命中一级缓存无需 onCreateViewHolder 和 onBindViewHolder
      • mAttachScrap 在 adapter.notifyXxx 的时候用到
      • mChanedScarp 在每次 View 绘制的时候用到,因为 getViewHolderForPosition 非调用多次,后面将
      • mCachedView:用来解决滑动抖动的情况,默认值为 2
    • 二级缓存:返回 View

      • 按照 position 和 type 进行匹配
      • 直接返回 View
      • 需要自己继承 ViewCacheExtension 实现
      • 位置固定,内容不发生改变的情况,比如说 Header 如果内容固定,就可以使用
    • 三级缓存:返回布局有效,内容无效的 ViewHolder

      • 按照 type 进行匹配,每个 type 缓存值默认 =5
      • layout 是有效的,但是内容是无效的
      • 多个 RecycleView 可共享, 可用于多个 RecyclerView 的优化
  • 图解博客

25.0.1.0 屏幕滑动 (状态是 item 状态可见,不可见,即将可见变化) 时三级缓存是如何理解的?adapter 中的几个方法是如何变化?

  • 屏幕滑动 (状态是 item 状态可见,不可见,即将可见变化) 时三级缓存是如何理解的?

    • 如图所示

    • 实例解释:

      • 由于 ViewCacheExtension 在实际使用的时候较少用到,因此本例中忽略二级缓存。mChangedScrap 和 mAttchScrap 是 RecyclerView 内部控制的缓存,本例暂时忽略。
      • 图片解释:

        • RecyclerView 包含三部分:已经出屏幕,在屏幕里面,即将进入屏幕,我们滑动的方向是向上
        • RecyclerView 包含三种 Type:1,2,3。屏幕里面的都是 Type=3
        • 红色的线代表已经出屏幕的 ViewHolder 与 Recycler 的交互情况
        • 绿色的线代表,即将进入屏幕的 ViewHolder 进入屏幕时候,ViewHolder 与 Recycler 的交互情况
      • 出屏幕时候的情况

        • 当 ViewHolder(position=0,type=1)出屏幕的时候,由于 mCacheViews 是空的,那么就直接放在 mCacheViews 里面,ViewHolder 在 mCacheViews 里面布局和内容都是有效的,因此可以直接复用。
        ViewHolder(position=1,type=2)同步骤 1
        - 当 ViewHolder(position=2,type=1)出屏幕的时候由于一级缓存 mCacheViews 已经满了,因此将其放入 RecyclerPool(type=1)的缓存池里面。此时 ViewHolder 的内容会被标记为无效,当其复用的时候需要再次通过 Adapter.bindViewHolder 来绑定内容。ViewHolder(position=3,type=2)同步骤 3
    - 进屏幕时候的情况[博客](https://github.com/yangchong211/YCBlogs)
        - 当 ViewHolder(position=3-10,type=3)进入屏幕绘制的时候,由于 Recycler 的 mCacheViews 里面找不到 position 匹配的 View,同时 RecyclerPool 里面找不到 type 匹配的 View,因此,其只能通过 adapter.createViewHolder 来创建 ViewHolder,然后通过 adapter.bindViewHolder 来绑定内容。- 当 ViewHolder(position=11,type=1)进入屏幕的时候,发现 ReccylerPool 里面能找到 type= 1 的缓存,因此直接从 ReccylerPool 里面取来使用。由于内容是无效的,因此还需要调用 bindViewHolder 来绑定布局。同时 ViewHolder(position=4,type=3)需要出屏幕,其直接进入 RecyclerPool(type=3)的缓存池中
        - ViewHolder(position=12,type=2)同步骤 6
    - 屏幕往下拉 ViewHolder(position=1)进入屏幕的情况
        - 由于 mCacheView 里面的有 position= 1 的 ViewHolder 与之匹配,直接返回。由于内容是有效的,因此无需再次绑定内容
        - ViewHolder(position=0)同步骤 8


25.0.1.1 SnapHelper 有哪些重要的方法,其作用就是是什么?LinearSnapHelper 中是如何实现滚动停止的?

  • SnapHelper 有哪些重要的方法,其作用就是是什么?

    • calculateDistanceToFinalSnap 抽象方法

      • 计算最终对齐要移动的距离

        • 计算二个参数对应的 ItemView 当前的坐标与需要对齐的坐标之间的距离。该方法返回一个大小为 2 的 int 数组,分别对应 out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
        @NonNull View targetView);
- findSnapView 抽象方法
    - 找到要对齐的 View
        - 该方法会找到当前 layoutManager 上最接近对齐位置的那个 view,该 view 称为 SanpView,对应的 position 称为 SnapPosition。如果返回 null,就表示没有需要对齐的 View,也就不会做滚动对齐调整。```
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);
    ```
- findTargetSnapPosition 抽象方法
    - 找到需要对齐的目标 View 的的 Position。[博客](https://github.com/yangchong211/YCBlogs)
        - 更加详细一点说就是该方法会根据触发 Fling 操作的速率(参数 velocityX 和参数 velocityY)来找到 RecyclerView 需要滚动到哪个位置,该位置对应的 ItemView 就是那个需要进行对齐的列表项。我们把这个位置称为 targetSnapPosition,对应的 View 称为 targetSnapView。如果找不到 targetSnapPosition,就返回 RecyclerView.NO_POSITION。```
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
            int velocityY);
    ```
  • LinearSnapHelper 中是如何实现滚动停止的?

    • SnapHelper 继承了 RecyclerView.OnFlingListener,实现了 onFling 方法。

      • 获取 RecyclerView 要进行 fling 操作需要的最小速率,为啥呢?因为只有超过该速率,ItemView 才会有足够的动力在手指离开屏幕时继续滚动下去。该方法返回的是一个布尔值!
      @Override
      public boolean onFling(int velocityX, int velocityY) {LayoutManager layoutManager = mRecyclerView.getLayoutManager();
          if (layoutManager == null) {return false;}
          RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
          if (adapter == null) {return false;}
          int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
          return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                  && snapFromFling(layoutManager, velocityX, velocityY);
      }
    • 接着看看 snapFromFling 方法源代码,就是通过该方法实现平滑滚动并使得在滚动停止时 itemView 对齐到目的坐标位置

      • 首先 layoutManager 必须实现 ScrollVectorProvider 接口才能继续往下操作
      • 然后通过 createSnapScroller 方法创建一个 SmoothScroller,这个东西是一个平滑滚动器,用于对 ItemView 进行平滑滚动操作
      • 根据 x 和 y 方向的速度来获取需要对齐的 View 的位置,需要子类实现
      • 最终通过 SmoothScroller 来滑动到指定位置博客
      private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
              int velocityY) {if (!(layoutManager instanceof ScrollVectorProvider)) {return false;}
      
          RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
          if (smoothScroller == null) {return false;}
      
          int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
          if (targetPosition == RecyclerView.NO_POSITION) {return false;}
      
          smoothScroller.setTargetPosition(targetPosition);
          layoutManager.startSmoothScroll(smoothScroller);
          return true;
      }
      • 总结一下可知:snapFromFling()方法会先判断 layoutManager 是否实现了 ScrollVectorProvider 接口,如果没有实现该接口就不允许通过该方法做滚动操作。接下来就去创建平滑滚动器 SmoothScroller 的一个实例,layoutManager 可以通过该平滑滚动器来进行滚动操作。SmoothScroller 需要设置一个滚动的目标位置,将通过 findTargetSnapPosition()方法来计算得到的 targetSnapPosition 给它,告诉滚动器要滚到这个位置,然后就启动 SmoothScroller 进行滚动操作。
    • 接着看下 createSnapScroller 这个方法源码博客

      • 先判断 layoutManager 是否实现了 ScrollVectorProvider 这个接口,没有实现该接口就不创建 SmoothScroller
      • 这里创建一个 LinearSmoothScroller 对象,然后返回给调用函数,也就是说,最终创建出来的平滑滚动器就是这个 LinearSmoothScroller
      • 在创建该 LinearSmoothScroller 的时候主要考虑两个方面:

        • 第一个是滚动速率,由 calculateSpeedPerPixel()方法决定;
        • 第二个是在滚动过程中,targetView 即将要进入到视野时,将匀速滚动变换为减速滚动,然后一直滚动目的坐标位置,使滚动效果更真实,这是由 onTargetFound()方法决定。
@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {if (!(layoutManager instanceof ScrollVectorProvider)) {return null;}
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
        @Override
        protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                    targetView);
            final int dx = snapDistances[0];
            final int dy = snapDistances[1];
            final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
            if (time > 0) {action.update(dx, dy, time, mDecelerateInterpolator);
            }
        }

        @Override
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;}
    };
}

25.0.1.2 LinearSnapHelper 代码中 calculateDistanceToFinalSnap 作用是什么?那么 out[0]和 out[1]分别指什么?

  • calculateDistanceToFinalSnap 的作用是什么

    • 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为 0
    • 如果是竖直方向滚动的,则计算竖直方向需要移动的距离,否则竖直方向的移动距离为 0
    • distanceToCenter 方法主要作用是:计算水平或者竖直方向需要移动的距离
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {int[] out = new int[2];
        if (layoutManager.canScrollHorizontally()) {out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {out[0] = 0;
        }
    
        if (layoutManager.canScrollVertically()) {out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {out[1] = 0;
        }
        return out;
    }
    • 接着看看 distanceToCenter 方法

      • 计算对应的 view 的中心坐标到 RecyclerView 中心坐标之间的距离
      • 首先是找到 targetView 的中心坐标
      • 接着也就是找到容器【RecyclerView】的中心坐标
      • 两个中心坐标的差值就是 targetView 需要滚动的距离
      private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
              @NonNull View targetView, OrientationHelper helper) {final int childCenter = helper.getDecoratedStart(targetView)
                  + (helper.getDecoratedMeasurement(targetView) / 2);
          final int containerCenter;
          if (layoutManager.getClipToPadding()) {containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;} else {containerCenter = helper.getEnd() / 2;
          }
          return childCenter - containerCenter;
      }
  • 那么 out[0]和 out[1]分别指什么

    • 返回的是一个长度为 2 的 int 数组 out,out[0]是 x 方向对齐要移动的距离,out[1]是 y 方向对齐要移动的距离。

25.0.1.3 如何实现可以设置分割线的颜色,宽度,以及到左右两边的宽度间距的自定义分割线,说一下思路?

  • 需要实现的分割线功能

    • 可以设置分割线的颜色,宽度,以及到左右两边的宽度间距。item 默认分割线的颜色不可改变,那么只有重写 onDraw 方法,通过设置画笔 point 颜色来绘制分割线颜色。而设置分割线左右的间隔是通过 getItemOffsets 方法实现的。
  • 几个重要的方法说明

    • 需要自定义类实现 RecyclerView.ItemDecoration 类,并选择重写合适方法。注意下面这三个方法有着强烈的因果关系!
    // 获取当前 view 的位置信息,该方法主要是设置条目周边的偏移量
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
    // 在 item 背后 draw
    public void onDraw(Canvas c, RecyclerView parent, State state)
    // 在 item 上边 draw
    public void onDrawOver(Canvas c, RecyclerView parent, State state)
  • 注意的是三个方法的调用顺序

    • 首先调用的是 getItemOffsets 会被多次调用,在 layoutManager 每次测量可摆放的 view 的时候回调用一次,在当前状态下需要摆放多少个 view 这个方法就会回调多少次。
    • 其次会调用 onDraw 方法,ItemDecoration 的 onDraw 方法是在 RecyclerView 的 onDraw 方法中调用的,注意这时候传入的 canvas 是 RecyclerView 的 canvas,要时刻注意这点,它是和 RecyclerView 的边界是一致的。这个时候绘制的内容相当于背景,会被 item 覆盖。
    • 最后调用的是 onDrawOver 方法,ItemDecoration 的 onDrawOver 方法是在 RecyclerView 的 draw 方法中调用的,同样传入的是 RecyclerView 的 canvas,这时候 onlayout 已经调用,所以此时绘制的内容会覆盖 item。
  • 为每个 item 实现索引的思路

    • 要实现上面的可以设置分割线颜色和宽度,肯定是要绘制的,也就是需要使用到 onDraw 方法。那么在 getItemOffsets 方法中需要让 view 摆放位置距离 bottom 的距离是分割线的宽度。博客
    • 然后通过 parent.getChildCount()方法拿到当前显示的 view 的数量[注意,该方法并不会获取不显示的 view 的数量],循环遍历后,直接用 paint 画笔进行绘制[注意至于分割线的颜色就是需要设置画笔的颜色]。

25.0.1.4 如何实现复杂 type 首页需求?如果不封装会出现什么问题和弊端?如何提高代码的简便性和高效性?

  • 如何实现复杂 type 首页需求

    • 通常写一个多 Item 列表的方法

      • 根据不同的 ViewType 处理不同的 item,如果逻辑复杂,这个类的代码量是很庞大的。如果版本迭代添加新的需求,修改代码很麻烦,后期维护困难。
    • 主要操作步骤

      • 在 onCreateViewHolder 中根据 viewType 参数,也就是 getItemViewType 的返回值来判断需要创建的 ViewHolder 类型
      • 在 onBindViewHolder 方法中对 ViewHolder 的具体类型进行判断,分别为不同类型的 ViewHolder 进行绑定数据与逻辑处理
    • 代码如下所示

      public class HomeAdapter extends RecyclerView.Adapter {
          public static final int TYPE_BANNER = 0;
          public static final int TYPE_AD = 1;
          @Override
          public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {switch (viewType){
                  case TYPE_BANNER:
                      return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_banner_layout,null));
                  case TYPE_AD:
                      return new BannerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_ad_item_layout,null));
              }
              return null;
          }
      
          @Override
          public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {int type = getItemViewType(position);
              switch (type){
                  case TYPE_BANNER:
                      // banner 逻辑处理
                      break;
                  case TYPE_AD:
                      // 广告逻辑处理
                      break;
                  // ... 此处省去 N 行代码
              }
          }
      
          @Override
          public int getItemViewType(int position) {if(position == 0){return TYPE_BANNER;//banner 在开头}else {return mData.get(position).type;//type 的值为 TYPE_AD,TYPE_IMAGE,TYPE_AD,等其中一个
              }
          }
          public static class BannerViewHolder extends RecyclerView.ViewHolder{public BannerViewHolder(View itemView) {super(itemView);
              }
          }
          public static class NewViewHolder extends RecyclerView.ViewHolder{public VideoViewHolder(View itemView) {super(itemView);
              }
          }
      }
  • 如果不封装会出现什么问题和弊端

    • RecyclerView 可以用 ViewType 来区分不同的 item, 也可以满足需求,但还是存在一些问题,比如:

      • 1,在 item 过多逻辑复杂列表界面,Adapter 里面的代码量庞大,逻辑复杂,后期难以维护。
      • 2,每次增加一个列表都需要增加一个 Adapter,重复搬砖,效率低下。
      • 3,无法复用 adapter,假如有多个页面有多个 type,那么就要写多个 adapter。
      • 4,要是有局部刷新,那么就比较麻烦了,比如广告区也是一个九宫格的 RecyclerView,点击局部刷新当前数据,比较麻烦。
    • 上面那样写的弊端

      • 类型检查与类型转型,由于在 onCreateViewHolder 根据不同类型创建了不同的 ViewHolder,所以在 onBindViewHolder 需要针对不同类型的 ViewHolder 进行数据绑定与逻辑处理,这导致需要通过 instanceof 对 ViewHolder 进行类型检查与类型转型。
      • 不利于扩展,目前的需求是列表中存在 5 种布局类类型,那么如果需求变动,极端一点的情况就是数据源是从服务器获取的,数据中的 model 决定列表中的布局类型。这种情况下,每当 model 改变或 model 类型增加,我们都要去改变 adapter 中很多的代码,同时 Adapter 还必须知道特定的 model 在列表中的位置(position)除非跟服务端约定好,model(位置)不变,很显然,这是不现实的。
      • 不利于维护,这点应该是上一点的延伸,随着列表中布局类型的增加与变更,getItemViewType、onCreateViewHolder、onBindViewHolder 中的代码都需要变更或增加,Adapter 中的代码会变得臃肿与混乱,增加了代码的维护成本。
  • 如何提高代码的简便性和高效性。具体封装库看:recyclerView 复杂 type 封装库

    • 核心目的就是三个

      • 避免类的类型检查与类型转型
      • 增强 Adapter 的扩展性
      • 增强 Adapter 的可维护性
    • 当列表中类型增加或减少时 Adapter 中主要改动的就是 getItemViewType、onCreateViewHolder、onBindViewHolder 这三个方法,因此,我们就从这三个方法中开始着手。
    • 既然可能存在多个 type 类型的 view,那么能不能把这些比如 banner,广告,文本,视频,新闻等当做一个 HeaderView 来操作。
    • 在 getItemViewType 方法中。

      • 减少 if 之类的逻辑判断简化代码,可以简单粗暴的用 hashCode 作为增加 type 标识。
      • 通过创建列表的布局类型,同时返回的不再是简单的布局类型标识,而是布局的 hashCode 值
    • onCreateViewHolder

      • getItemViewType 返回的是布局 hashCode 值,也就是 onCreateViewHolder(ViewGroup parent, int viewType)参数中的 viewType
    • 在 onBindViewHolder 方法中。可以看到,在此方法中,添加一种 header 类型的 view,则通过 onBindView 进行数据绑定。
    • 封装后好处

      • 拓展性——Adapter 并不关心不同的列表类型在列表中的位置,因此对于 Adapter 来说列表类型可以随意增加或减少。十分方便,同时设置类型 view 的布局和数据绑定都不需要在 adapter 中处理。充分解耦。
      • 可维护性——不同的列表类型由 adapter 添加 headerView 处理,哪怕添加多个 headerView,相互之间互不干扰,代码简洁,维护成本低。

25.0.1.5 关于 item 条目点击事件在 onCreateViewHolder 中写和在 onBindViewHolder 中写有何区别?如何优化?

  • 关于 rv 设置 item 条目点击事件有两种方式:1. 在 onCreateViewHolder 中写;2. 在 onBindViewHolder 中写;3. 在 ViewHolder 中写。那么究竟是哪一种好呢?

    • 1. 在 onCreateViewHolder 中写

      @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;
      }
    • 2. 在 onBindViewHolder 中写

      @Override
      public void onBindViewHolder(@NonNull final MyViewHolder holder, int position) {holder.itemView.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {if (listener != null) {listener.onItemClick(holder.itemView, holder.getAdapterPosition());
                  }
              }
          });
      }
  • onBindViewHolder() 中频繁创建新的 onClickListener 实例没有必要,建议实际开发中应该在 onCreateViewHolder() 中每次为新建的 View 设置一次就行。

25.0.1.6 RecyclerView 滑动卡顿原因有哪些?如何解决嵌套布局滑动冲突?如何解决 RecyclerView 实现画廊卡顿?

  • RecyclerView 滑动卡顿原因有哪些

    • 第一种:嵌套布局滑动冲突

      • 导致嵌套滑动难处理的关键原因在于当子控件消费了事件, 那么父控件就不会再有机会处理这个事件了, 所以一旦内部的滑动控件消费了滑动操作, 外部的滑动控件就再也没机会响应这个滑动操作了
    • 第二种:嵌套布局层次太深,比如六七层等

      • 测量,绘制布局可能会导致滑动卡顿
    • 第三种:比如用 RecyclerView 实现画廊,加载比较大的图片,如果快速滑动,则可能会出现卡顿,主要是加载图片需要时间
    • 第四种:在 onCreateViewHolder 或者在 onBindViewHolder 中做了耗时的操作导致卡顿。
  • 如何解决嵌套布局滑动冲突

    • 这个具体看我的博客:23.RecyclerView 滑动冲突
  • 如何解决 RecyclerView 实现画廊卡顿?

    • RecyclerView 滑动时不让 Glide 加载图片。滚动停止后才开始恢复加载图片。
    //RecyclerView.SCROLL_STATE_IDLE // 空闲状态
    //RecyclerView.SCROLL_STATE_FLING // 滚动状态
    //RecyclerView.SCROLL_STATE_TOUCH_SCROLL // 触摸后状态
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {LoggerUtils.e("initRecyclerView"+ "恢复 Glide 加载图片");
                Glide.with(ImageBrowseActivity.this).resumeRequests();}else {LoggerUtils.e("initRecyclerView"+"禁止 Glide 加载图片");
                Glide.with(ImageBrowseActivity.this).pauseRequests();}
        }
    });
  • 在 onCreateViewHolder 或者在 onBindViewHolder 中做了耗时的操作导致卡顿

    • 按 stackoverflow 上面比较通俗的解释:RecyclerView.Adapter 里面的 onCreateViewHolder()方法和 onBindViewHolder()方法对时间都非常敏感。类似 I / O 读写,Bitmap 解码一类的耗时操作,最好不要在它们里面进行。

25.0.1.7 RecyclerView 常见的优化有哪些?实际开发中都是怎么做的,优化前后对比性能上有何提升?

  • RecyclerView 常见的优化有哪些

    • DiffUtil 刷新优化

      • 分页拉取远端数据,对拉取下来的远端数据进行缓存,提升二次加载速度;对于新增或者删除数据通过 DiffUtil 来进行局部刷新数据,而不是一味地全局刷新数据。
    • 布局优化

      • 减少 xml 文件 inflate 时间

        • 这里的 xml 文件不仅包括 layout 的 xml,还包括 drawable 的 xml,xml 文件 inflate 出 ItemView 是通过耗时的 IO 操作,尤其当 Item 的复用几率很低的情况下,随着 Type 的增多,这种 inflate 带来的损耗是相当大的,此时我们可以用代码去生成布局,即 new View() 的方式,只要搞清楚 xml 中每个节点的属性对应的 API 即可。
      • 减少 View 对象的创建

        • 一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套。博客
    • 对 itemView 中孩子 View 的点击事件优化

      • onBindViewHolder() 中频繁创建新的 onClickListener 实例没有必要,建议实际开发中应该在 onCreateViewHolder() 中每次为新建的 View 设置一次就行。
  • 其他的一些优化点

    • 如果 Item 高度是固定的话,可以使用 RecyclerView.setHasFixedSize(true); 来避免 requestLayout 浪费资源;

      • 具体看 Understanding RecyclerView setHasFixedSize
    • 设置 RecyclerView.addOnScrollListener(listener); 来对滑动过程中停止加载的操作。
    • 如果不要求动画,可以通过 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false); 把默认动画关闭来提神效率。
    • 通过重写 RecyclerView.onViewRecycled(holder) 来回收资源。
    • 通过 RecycleView.setItemViewCacheSize(size); 来加大 RecyclerView 的缓存,用空间换时间来提高滚动的流畅性。
    • 如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool); 来共用一个 RecycledViewPool。

25.0.1.8 如何解决 RecyclerView 嵌套 RecyclerView 条目自动上滚的 Bug?如何解决 ScrollView 嵌套 RecyclerView 滑动冲突?

  • RecyclerView 嵌套 RecyclerView 条目自动上滚的 Bug

    • RecyclerViewA 嵌套 RecyclerViewB 进入页面自动跳转到 RecyclerViewB 上面页面会自动滚动。
    • 解决办法如下所示
    • 一,recyclerview 去除焦点

      • recyclerview.setFocusableInTouchMode(false);
      • recyclerview.requestFocus();
    • 二,在代码里面 让处于 ScrollView 或者 RecyclerView1 顶端的某个控件获得焦点即可

      • 比如顶部的一个 textview
      • tv.setFocusableInTouchMode(true);
      • tv.requestFocus();
    • 三,可以直接在 RecyclerView 父布局中添加上 descendantFocusability 属性的值有三种:android:descendantFocusability=”beforeDescendants”

      beforeDescendants:viewgroup 会优先其子类控件而获取到焦点
      afterDescendants:viewgroup 只有当其子类控件不需要获取焦点时才获取焦点
      blocksDescendants:viewgroup 会覆盖子类控件而直接获得焦点
  • 如何解决 ScrollView 嵌套 RecyclerView 滑动冲突?

    • 第一种方式:

      • 重写父控件,让父控件 ScrollView 直接拦截滑动事件,不向下分发给 RecyclerView,具体是定义一个 ScrollView 子类,重写其 onInterceptTouchEvent()方法
      public class NoNestedScrollview extends NestedScrollView {
          @Override
          public boolean onInterceptTouchEvent(MotionEvent e) {int action = e.getAction();
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                      downX = (int) e.getRawX();
                      downY = (int) e.getRawY();
                      break;
                  case MotionEvent.ACTION_MOVE:
                      // 判断是否滑动,若滑动就拦截事件
                      int moveY = (int) e.getRawY();
                      if (Math.abs(moveY - downY) > mTouchSlop) {return true;}
                      break;
                  default:
                      break;
              }
              return super.onInterceptTouchEvent(e);
          }
      }
    • 第二种解决方式博客

      • a. 禁止 RecyclerView 滑动
      recyclerView.setLayoutManager(new GridLayoutManager(mContext,2){
          @Override
          public boolean canScrollVertically() {return false;}
          
          @Override
          public boolean canScrollHorizontally() {return super.canScrollHorizontally();
          }
      });
      
      recyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayout.VERTICAL,false){
          @Override
          public boolean canScrollVertically() {return false;}
      });
    • 可能会出现的问题 博客

      • 虽然上面两种方式解决了滑动冲突,但是有的手机上出现了 RecyclerView 会出现显示不全的情况。
      • 针对这种情形,使用网上的方法一种是使用 RelativeLayout 包裹 RecyclerView 并设置属性:android:descendantFocusability=”blocksDescendants”

        • android:descendantFocusability=”blocksDescendants”,该属 > 性是当一个 view 获取焦点时,定义 ViewGroup 和其子控件直接的关系,常用来 > 解决父控件的焦点或者点击事件被子空间获取。
        • beforeDescendants: ViewGroup 会优先其子控件获取焦点
        • afterDescendants: ViewGroup 只有当其子控件不需要获取焦点时才获取焦点
        • blocksDescendants: ViewGroup 会覆盖子类控件而直接获得焦点
      • 相关代码案例:https://github.com/yangchong2…
      <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:descendantFocusability="blocksDescendants">
          <android.support.v7.widget.RecyclerView
              android:id="@+id/rv_hot_review"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:foregroundGravity="center" />
      </RelativeLayout>

25.0.1.9 如何处理 ViewPager 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager?如何解决 RecyclerView 使用 Glide 加载图片导致图片错乱问题?

  • ViewPager 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager

    • 继承 RecyclerView,重写 dispatchTouchEvent,根据 ACTION_MOVE 的方向判断是否调用 getParent().requestDisallowInterceptTouchEvent 去阻止父 view 拦截点击事件
    @Override 
    public boolean dispatchTouchEvent(MotionEvent ev) { 
        /*--- 解决垂 ViewPager 嵌套直 RecyclerView 嵌套水平 RecyclerView 横向滑动到底后不滑动 ViewPager start ---*/ 
        ViewParent parent = this; 
        while(!((parent = parent.getParent()) instanceof ViewPager));
        // 循环查找 viewPager 
        parent.requestDisallowInterceptTouchEvent(true); 
        return super.dispatchTouchEvent(ev); 
    }
  • 如何解决 RecyclerView 使用 Glide 加载图片导致图片错乱问题

    • 为何会导致图片加载后出现错乱效果

      • 因为有 ViewHolder 的重用机制,每一个 item 在移除屏幕后都会被重新使用以节省资源,避免滑动卡顿。而在图片的异步加载过程中,从发出网络请求到完全下载并加载成 Bitmap 的图片需要花费很长时间,而这时候很有可能原先需要加载图片的 item 已经划出界面并被重用了。而原先下载的图片在被加载进 ImageView 的时候没有判断当前的 ImageView 是不是原先那个要求加载的,故可能图片被加载到被重用的 item 上,就产生了图片错位的问题。解决思路也很简单,就是在下载完图片,准备给 ImageView 装上的时候检查一下这个 ImageView。博客
    • 第一种方法

      • 使用 settag()方式,这种方式还是比较好的,但是,需要注意的是,Glide 图片加载也是使用将这个方法的,所以当你在 Bindviewholder()使用时会直接抛异常,你需要使用 settag(key,value)方式进行设置,这种方式是不错的一种解决方式,注意取值的时候应该是 gettag(key)这个方法哈,当异步请求回来的时候对比下 tag 是否一样在判断是否显示图片。这边直接复制博主的代码了。
      // 给 ImageView 打上 Tag 作为特有标记
      imageView.setTag(tag);
       
      // 下载图片
      loadImage();
       
      // 根据 tag 判断是不是需要设置给 ImageView
      if(tag == iamgeView.getTag()) {imageView.setBitmapImage(iamge);
      }

项目开源地址:https://github.com/yangchong2…

退出移动版