一行代码搞定 Android 复杂列表埋点曝光

一行代码搞定 Android 复杂列表埋点曝光,第1张

一个好的产品离不开数据分析,在手机 APP 中,数据分析极致化需要细致到某个时刻列表曝光的了哪几个 Item。

2022 年了,基本上目前 Android 上可以滑动的复杂列表都是 RecyclerView 或者其扩展,这里分享一个封装的思路。

如果非要细化细节:

各种方案核心都差不多,最关键的就是通过 LayoutManager 获取屏幕内第一个可见和最后一个可见 item position,上报其区间内的 Item。这里简称这个逻辑为 检查上报逻辑

但是触发时机有所不同,通常如下方案一和二所述,当然除了方案一和方案二外,还有一些别的方案,比如监听 RecyclerView 的布局树变化触发 检查上报逻辑 等方案。

可以发现方案二相比方案一更有利于减少各种回调的注册和周期的控制,下文会在方案二的基础上,阐述用法和相关实现思路。

仓库地址: RecyclerViewExposure

这里会主要说明一些主要逻辑,需要完整的逻辑可以 fork 仓库 查看

思路来自于 lifecycle 的设计,这里主要是想让 Activity/Fragment 提供可见和不可见的状态变化给外部订阅

对 List Item 的收集处理是 RecyclerViewExposure 最核心的收集数据逻辑,这里针对在 Activity 的使用作为例子。上文已经讲述如何做一个 PageLifeCycleHolder 为其他组件提供页面可见状态,下文将直接使用。

Recycler类是RecyclerView内部final类,它管理scrapped(废弃)或detached(独立)的Item视图,使它们可以重用。我们都知道,在ListView中,也有一个类似的RecycleBin类,管理Item的重用。 本文的重点是Recycler类,分析一下视图在消失与出现时,如何利用Recycler实现重用。

ViewHolder类RecyclerView的内部抽象类,我们自己定义的Adapter中实现,封装子视图的一些视图。

先看一下Recycler内部的几个引用。

mAttachedScrap列表: 用来存储Scrapped(废弃)的ViewHolder,它对应的视图是detached的,即ItemView调用了ViewGroup的detachViewFromParent方法,从容器的子视图数组中移除,它其实并没有被废弃。它正是存放从RecyclerView中detached的ItemView的ViewHolder列表。

当RecyclerView初始加载Item,第一次触发onLayoutChildren时,fill创建满足RecyclerView高度的子ItemView,ViewHolder绑定ItemView,并ViewGroup#addView加入RecyclerView视图。第二次onLayoutChildren时,通过detachAndScrapAttachedViews方法将全部ItemView从mChildren数组删除,触发的是ViewGroup#detachViewFromParent方法,ItemView变为detached,ViewHolder放入mAttachedScrap,fill继续触发从mAttachedScrap中获取ViewHolder,将ViewHolder加入子View数组,触发的是ViewGroup#attachViewToParent方法。

mCachedViews列表: 从RecyclerView区域移除,从ViewGroup中删除的ItemView,存储在列表中,最大值max,大于max时,删除最早进入的第0个元素,该元素放入RecycledViewPool中,如果还是放不下,直接放入RecycledViewPool。 永远存储最新从RecyclerView删除的视图ViewHolder。

ViewGroup已经执行过removeViewAt删除了View 。

RecycledViewPool: 视图缓存池,当mCachedViews存储不下时,将ViewHolder放入,根据类型存储。ViewGroup已经执行过removeViewAt删除了View。

ViewCacheExtension: 扩展使用,开发者自己控制缓存。

图中的数据源一共有17项,显示区域中,可容纳的子视图大约在12个左右。

RecyclerView视图显示出来以后,手指触屏,向上滑动。此时,position是0,1,2,3的ItemView依次滚动出视图可见范围。

通过源码调试, 发现在LinearLayoutManager的recycleChildren方法处, 触发了下面的方法,定义在LayoutManager类。

首先,LayoutManager的removeViewAt方法, 从RecyclerView中删除索引index的子视图,它与position无关。调用辅助类ChildHelper的removeViewAt方法。

RecyclerView类的初始化initChildrenHelper方法,定义Callback对象,在辅助类的方法中, 调用内部Callback的对应方法。

dispatchChildDetached方法, 通知子视图detached,将调用Adapter的onViewDetachedFromWindow方法, 可以在自己的Adapter中重写。注意,这里并没有触发ViewGroup的detachViewFromParent方法。

RecyclerView的removeViewAt方法,调用父类ViewGroup的removeViewAt方法,删除该ItemView子视图。

手指上滑,每次最顶部Item视图滑出屏幕时,删除的都是index是0的子视图,手指下移,每次底部Item视图滑出可视范围,删除的都是index是12左右的子视图, 与position无关。

其次,调用Recycler的recycleView方法, 将ViewHolder加入缓存mCachedViews或RecycledViewPool池。

根据View获取它绑定的ViewHolder对象, 从View的LayoutParams中获取。 ViewHolder的内部mScrapContainer(即Recycler)是空,isScrap方法返回false。只有执行过Recycler的scrapView(View)方法,将ViewHolder加入到mAttachedScrap列表时,才会设置内部mScrapContainer值,当isScrap返回true时,调用unScrap方法, 调用内部Recycler的unscrapView方法。

从mAttachedScrap列表中删除,置空ViewHolder内部Recycler。

Recycler的recycleViewHolderInternal方法, 将ViewHolder加入缓存mCachedViews或RecycledViewPool池。

待加入的ViewHolder不能是Scrap,前面经过unScrap方法处理过。缓存mCachedViews最大值是mViewCacheMax,当达到最大时,删除第一个,被删除元素加入RecycledViewPool。如果数量已经小于最大值,将新ViewHolder放入mCachedViews缓存,如果仍然大于, 将其放入RecycledViewPool。

将ViewHolder所属的RecyclerView置空,执行dispatchViewRecycled回调, 该方法将调用Adapter的onViewRecycled方法,可重写。 ViewHolder放置到RecycledViewPool缓存池。

手指触屏,向上滑动,position是12,13,14,15的ItemView依次从底部冒出,通过调试源码, 调用Recycler的getViewForPosition方法。 该方法根据position获取ItemView视图,position是RecyclerView的数据源索引,当视图完全展示后,子视图有12个,那么,最后一个的索引是11,position是12索引对应视图不可见,上滑时,12索引首先出现。

首先, 从mAttachedScrap与mCachedViews中查找ViewHolder,在视图滚动时,mAttachedScrap是空的,因此,一般情况从mCachedViews缓存查找。

validateViewHolderForOffsetPosition方法, 验证holder是否可用于对应position索引。如果验证通过,设置fromScrap标志,返回holder的itemView视图。如果验证失败,将增加无效标志,holder内部mScrapContainer(即Recycler)存在,说明holder是isScrap的 ,Scrap的holder无法被回收,unScrap方法提前去除其标志,最后会加入缓存,recycleViewHolderInternal方法。

其次, 从RecycledViewPool缓存池中查找。从这里获取的ViewHolder,设置mPosition是NO_POSITION(-1)。如果都未找到,通过Adapter的createViewHolder方法创建, 调用Adapter的onCreateViewHolder抽象方法,开发者重写此方法, 初始化ItemView,创建ViewHolder对象。最后,通过Adapter的bindViewHolder方法, 调用Adapter的onBindViewHolder抽象方法,开发者重写此方法。 初始化ViewHolder的View中数据。

Recycler的getScrapViewForPosition方法。

当视图滚动时,该方法从缓存mCachedViews查找ViewHolder,并且它的mPosition要和position一致。

举个例子说明一下。 假如position是12完全不可见,当向上滑动时,position是12的视图出现,此时,ViewHolder不是getScrapViewForPosition获取。因为mCachedViews还是空,或者position是0的视图已在mCachedViews缓存,但它的mPosition是0,与12不相等,也不会使用它。 因此,position是12的要新建ViewHolder。 当position是13和14视图出现,对应position是1,2的视图要进入mCachedViews缓存,如果mCachedViews缓存未达到最大值,将会一直新建ViewHolder,原因也一样,mPosition不符合。 如果到达最大值,缓存的最大值默认是2, 此时,已经存储position是0和1的值,继续上滑,position是2的视图要进缓存,删掉最早position是0的值,将它放入RecycledViewPool池。继续,position是14的出现,从缓存未找到符合的position,因为此刻缓存里还都是头部position较小的值,RecycledViewPool已经有值,就从RecycledViewPool获取。这里获取的与positon无关,ViewHoder的mPosition都是-1,只要type类型一样,在Adapter的bindViewHolder方法,会为mPosition赋值,这个ViewHolder内部mPosition就属于14啦。

改变方向手指下滑, position是2的视图出屏幕,对应的ViewHolder在缓存,直接使用。position是14的消失了,将position是14的ViewHolder加入缓存。

到这里,我们已经获取了屏幕下一个将要显示的ItemView,接下来就要将它加入到RecyclerView视图中, 调用LayoutManager#addViewInt方法。

如果发现ViewHolder的FLAG_RETURNED_FROM_SCRAP标志或isScrap,先unScrap处理,再调用ViewGroup的attachViewToParent方法。在滚动时,获取的isScrap是false。

借助ChildHelper的addView方法,调用CallBack的addView方法,最终,调用的是ViewGroup的addView,ItemView加入父容器,dispatchChildAttached方法,会触发Adapter的onViewAttachedToWindow方法。

ItemView帮助类,它通过内部Callback接口暴露出来,在RecyclerView类初始化ChildHelper时实现接口方法,调用RecyclerView的对应方法。处理子视图会借助父类ViewGroup。

任重而道远

尽量不要在onBindViewHolder方法中new监听器,在滑动屏幕时onBindViewHolder大量执行,此时会创建大量的监听器。

建议在ViewHolder的构造方法中设置监听器。

亦可以通过给ItemView设置Tag传值的方式所有Item共用一个监听器,如图:

执行逻辑:

创建需要显示的viewHolder(一般是比屏幕略多的数量,这些viewHolder将会被复用),

当有viewHolder滑出屏幕,并且新的viewHolder进入屏幕时,滑出屏幕的viewHolder可以通过onBindViewHolder进行重新绑定以显示对应potion数据源的视图。即,只要在onBindViewHolder中进行数据设置,每个显示在屏幕上的viewHolder都引用了对应数据源。

点击列表Item→引用该Item的ViewHolder收到点击事件→将当前ViewHolder通过setTag()方式让Item对其进行引用→监听器实现中通过getTag()的方式获取被点击Item绑定的ViewHolder→执行点击后的逻辑。

 1puteVerticalScrollOffset();然而compute方法计算出的并不是滑动的精确距离,stackOverflow上有答案解释其为 item 的平均高度 可见 item 数目,不是我们需要的精确距离。3 还有人说可以尝试getChildAt(0)[java] view plain copytotalDy = recyclerViewgetChildAt(0)getTop();依靠第一个item的滑动距离来进行动画的设置,但是根据该方法得出的 totalDy 在滑动到一定程度后清零。这是因为recyclerViewlgetChildAt(0) 返回的永远是第一个可见的child,不是所有view list 的第一个child,因此这种用法是得不到滑动距离的。另外下面这三种用法都是等价的,都是获取第一个可见的child:[java] view plain copyLinearLayoutManager layoutManager = (LinearLayoutManager) thisgetLayoutManager();View firstVisiableChildView = thisgetChildAt(0);View firstVisiableChildView = layoutManagergetChildAt(0)int position = layoutManagerfindFirstVisibleItemPosition();View firstVisiableChildView = layoutManagergetChildAt(position)但是下面这种就不是获取第一个可见的child,而是获得所有view list 的第一个child。但是滑动一段距离后它总是返回null,即第一个child被recycle后,总是返回null。[java] view plain copy//Don't use this function to get the first item, it will return null when the first item is recycledLinearLayoutManager layoutManager = (LinearLayoutManager) thisgetLayoutManager();View child2 = layoutManagerfindViewByPosition(0);4如果LayoutManager用的是LinearLayoutManager,强烈推荐下面的方法获取滑动距离:[java] view plain copypublic int getScollYDistance() {LinearLayoutManager layoutManager = (LinearLayoutManager) thisgetLayoutManager();int position = layoutManagerfindFirstVisibleItemPosition();View firstVisiableChildView = layoutManagerfindViewByPosition(position);int itemHeight = firstVisiableChildViewgetHeight();return (position) itemHeight - firstVisiableChildViewgetTop();}

以上就是关于一行代码搞定 Android 复杂列表埋点曝光全部的内容,包括:一行代码搞定 Android 复杂列表埋点曝光、RecyclerView回收机制分析、RecyclerView的Item控件监听器设置问题等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9503611.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-29
下一篇 2023-04-29

发表评论

登录后才能评论

评论列表(0条)

保存