本文所讲RecyclerView 是来自support 库 26 版本,本文主要来源于自身开发及组内同事遇到问题的经验总结,作为知识沉淀记录一下,以备日后查看。
本文主要讲解以下几部分:
(1)RecyclerView 滑动体验篇
1)横向ViewPager 与内嵌横向RecyclerView 之间的滑动冲突;
2)纵向RecycleView/ListView 与 横向RecycleView 之间的滑动冲突;
3)横向RecyclerView ItemView 滑动不停留在中间态;
4)记录、恢复RecyclerView 滚动偏移位置;
(2)RecyclerView 入坑篇
1)RecyclerView 导致的内存泄漏(support 26 + 7.0以下机型);
2)RecyclerView调用notifyDataSetChanged 会闪烁;
3)RecycleView /ListView 设置itemView 为View.GONE 效果等同于View.Invisible;
(3)RecyclerView 性能提升篇
一、RecyclerView 滑动体验篇
(1)ViewPager 与 横向RecyclerView 之间的滑动冲突
目前,企鹅FM项目中,很多页面使用ViewPager+ TabLayout (如首页、详情页、搜索结果页等),而对应页面很多时候会嵌套一个横向RecycleView,用来展现更多的信息,如下,在RecycleView中滑动到最后一个元素时,会同时带动ViewPager滑动,这种体验极差。
原因分析:
作为子View 的RecyclerView在滑到最后一个或第一个ItemView到导致ViewPager滑动,这一定是ViewPager在此刻对滑动事件进行了拦截,解决的最简单办法就是不让ViewPager拦截横向RecyclerView的滑动事件(即 ViewPager::onInterceptTouchEvent方法返回false),ViewPager::onInterceptTouchEvent中的Move 事件如下:
目前,有以下两种方式使ViewPager 不去拦截横向RecyclerView 滑动事件:
1)在RecyclerView 对应滑动事件分发中调用
getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewPager对其MOVE或者UP事件进行拦截,但是考虑的因素比较多,而且效果不是太好,故放弃这种方式。
2)修改某些方法,进入到上图if判断中
在滑动横向RecyclerView 到两端时,dx != 0 && !isGutterDrag(mLastMotionX, dx) 肯定满足条件,那说明canScroll() (用来判断一个View以及它的子View是否可以滑动)一定返回了false, 复写canScroll()方法,打log,发现返回果然为false,验证了自己的判断。
解决办法:复写canScroll,当View 是横向RecyclerView(LinearLayoutManager 包含GridLayoutManager)时,直接返回true即可解决问题,解决代码如下:
类似的冲突还有ViewPager 和HorizontalScrollView 等等,解决方式与上面类似。
(2)纵向RecyclerView/ListView 与 横向RecyclerView 之间的滑动冲突
在有些时候因为产品需求,需要在纵向的RecyclerView/ListView内嵌套一个横向的RecyclerView,当这个横向RecyclerView的item 比高度较大的时候(企鹅FM书城排行榜模块),在横向滑动时,容易导致整体向上滑,体验效果较差,如下图所示(网络盗图) :
造成上述现象的原因是:外层纵向滑动的RecyclerView对 横向滑动的RecyclerView 的滑动事件进行了拦截,如下图2 所示,canScrollVertically 此刻为true,因此这里仅仅只判断了Math.abs(dy)>mTouchSlop(可以认为是一个滑动阀值,是一个定值8dp) ,并未判断方向或角度,从而决定是否拦截。
解决办法 :既然RecyclerView::onInterceptTouchEvent 内部没有判断滑动角度或方向,那我们就人为去判断,在上面判读的基础上继续判断 Math.abs(dy) 和Math.abs(dx) 的大小,从而决定是否拦截:具体分析细节可参照 , 修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题
使用上述方法,可以很快解决上述滑动体验问题,那是不是只有上述一种解决方式了,答案是否定的,作为一名Android 开发者我们知道,除了上述方式拦截滑动事件外,我们还可以通过getParent().requestDisallowInterceptTouchEvent(true); 让父RecyclerView不去拦截横向滑动,如下是RecyclerView::onTouchEvent() ,内部已经实现了requestDisallowInterceptTouchEvent(true) 。
我们需要考虑的是,当我们横向上或横向下滑动时,需要 进入上图中1的判断 ,2的判断还未满足,此时内部横向RecyclerView 会拦截内部itemView的滑动事件,进而执行自己的onTouchEvent事件,从而调用requestDisallowInterceptTouchEvent(true) ,让外层RecyclerView不去拦截内部RecyclerView的横向滑动事件,至此需要解决如何保证先进入1判断而不进入2判断。
解决办法:通过调整TouchSlop值的大小
在开始我们已介绍RecyclerView 的默认TouchSlop 值是8dp,如果要先保证进入1判断条件,必须调大TouchSlop值(反射获取),经过调整TouchSlop (按倍数调整比较简单,可以先知道一个大致范围)验证,当TouchSlop扩大1倍时就能满足条件。
总结:上述两种方式各有优缺点,方法1,对原生RecyclerView 侵入性较强(特别是对RecyclerView 进行多层封装的情况下,影响比较大),优点是TouchSlop 值保持与系统一致,不会带来其他未知问题;方法 2 ,修改方式简单,入侵性小,缺点,需要调整TouchSlop 值,可能还会带来其他问题。
(3)横向RecyclerView ItemView 滑动不停留在中间态
如下图所示,正在滑动的模块是书城——排行榜模块,排行榜模块主要由横向RecyclerView 构成,内部包含两个榜单形式,列举前top3的内容,在(2)的基础上解决了纵向RecyclerView 嵌套横向RecyclerView 滑动问题外,还有有个小问题那就是,RecyclerView ItemView 滑动多少就停在那里,这种效果不是我们想要的,我们想要的是滑到左边就显示第一个榜单,滑到右边就显示第二个榜单。
那有没有好的办法做到这一点了,官方考虑到这一点,针对RecyclerView 滑动情况,专门提供了SnapHelper类(PagerSnapHelper 和LinearSnapHelper ,详细介绍介绍可参看Android中使用RecyclerView + SnapHelper实现类似ViewPager效果), 使用其他相当简单,针对上述问题解决方式如下:
(4)记录、恢复RecyclerView 滚动偏移位置
熟悉RecyclerView 缓存的同学应该知道(后面在也会介绍RecyclerView缓存机制),当RecyclerView中的itemView 滑出屏幕后会缓存在mCacheView 中(默认缓存最大数是2),因此当滑出屏幕超过2后,再滑回来,原来的位置信息都会被重置,对于一般的RecyclerView 没有什么影响,但是如果内嵌了一个横向RecyclerView (如下图中分类模块位置) ,起初”悬疑推理“ 在一排第一个位置,向左滑动到其他位置后,再纵向滑动外层RecyclerView ,发现分类模块第一个又变成了”悬疑推理“ ,这个是产品不能接受的。
那如何修正上述问题了,RecyclerView 布局 及位置相关信息都是由对应LayoutManager决定,因此查看对应LayoutManager::onSaveInstanceState() 如下所示,内部确实记录了position及offset 值。
解决办法步骤:
(1)在Adapter::onViewRecycled 中保存对应LayoutManager的onSaveInstanceState ,同时记录保存下来
(2)在setData()数据给Adapter 时,恢复对应LayoutManager 之前保存在数据信息
(3)保存记录RecyclerView 后的效果
二、RecyclerView 入坑篇
(1)RecyclerView 导致的内存泄漏(support 26 + 7.0以下机型)
在进行4.0 版本迭代时,发现在之前的广播聚合页存在RecyclerView导致的内存泄漏,下图为内存泄漏的引用链,引用对象可以追到GapWorker。这里的RecyclerView是一个横向的RecyclerView ,作为广播聚合页(ListView)的HeaderView。
由于广播页面是比较老的页面,最近几个版本也未发现此类泄漏,细细想一下,可能与RecyclerView 版本有关(4.0版本直接将support 库由23.1升级到26.1版本),刚好这几个版本,support 库 修复了修复很多RecyclerView 的bug 及添加了许多新功能。通过AndroidXRef 查询知(查询结果如下),GapWorker 果然是在support 26 新增的。
查看GapWorker ,里面sGapWorker 是一个ThreadLocal 带GapWorker 的对象,同时维持了一个RecyclerView 的List对象(通过add 和remove 方法进行)。
而GapWorker的add 和remove 方法分别在RecyclerView::onAttachedToWindow 和RecyclerView::onDetachedFromWindow 中调用,如下图所示:
根据上面的引用链知,RecyclerView::onDetachedFromWindow 方法 没有被主动调用,断点验证,在退出广播页面的时候也没有调用(导致泄漏),按理说在滑动离屏的时候就应该调用的,难道和RecylerView 做为ListView 的HeaderView 有关,顺着这条思路发现果然和上述使用方式有关。
之前组内同事chunyu遇到过:ListView 嵌套GridView时,GridView数据错乱问题(7.0及其以上有问题),里面刚好说明了7.0及其以上版本,官方修正了RecylerView 做为ListView 的HeaderView 情况,滑出屏幕,不调用onDetachedFromWindow()的原因,具体如下:
从分析中,可以获取到两个重要的信息:1)GapWorker 是在support 26 以上才有的,且SDK_INT>=21,才会进行对应add 和remove 操作 ;2)在SDK_INT< 24(7.0) 时,不会主动调用View::dispatchDetachedFromWindow()。
因此,上述问题的解决办是:在对应Fragment 的onDetach() 或 其他场景主要去调用上图中的ViewGroup::removeDetachedView() (这里需要使用反射),具体如下:
(2)RecyclerView调用notifyDataSetChanged 会闪烁
详见我的另一篇文章:RecyclerView notifyDataSetChanged 导致图片闪烁的真凶
(3)RecycleView /ListView 设置itemView 为View.GONE 效果等同于View.Invisible
解决办法:将itemView 的宽高设置成 0 ,重新设置一下LayoutParams
三、RecyclerView 性能提升篇
说是RecyclerView性能提升篇有点夸大 ,这里主要讲讲RecyclerView 使用小技巧
(1)setHasFixedSize(true)优化思想
(2)DiffUtil ()
(3)......
限于篇幅内容有点多,后续再补充..... ,有分析不对的地方欢迎指出 ,谢谢 ^_^!
相关引用资料
(1)修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题