阅读 9215

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的一般操作添加动画效果,如,增删条目等
  • 如图所示,直观展示结构
    • image
  • 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)方法
      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方法每次都会调用的
      • image
    • 查看一下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自定义控件源码分析,可以参考我的其他博客:github.com/yangchong21…
      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绘制原理如何理解?
    • image
  • 性能优化本质是什么?
    • 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的优化
  • 图解博客
    • image

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

  • 屏幕滑动(状态是item状态可见,不可见,即将可见变化)时三级缓存是如何理解的?
    • 如图所示
      • image
    • 实例解释:
      • 由于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
      • 进屏幕时候的情况博客
        • 当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。博客
        • 更加详细一点说就是该方法会根据触发 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中做了耗时的操作导致卡顿。
  • 如何解决嵌套布局滑动冲突
  • 如何解决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 浪费资源;
    • 设置 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会覆盖子类控件而直接获得焦点
      • 相关代码案例:github.com/yangchong21…
      <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);
      }
      复制代码

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

关注下面的标签,发现更多相似文章
评论