问题
PM需要获取当前条目的有效曝光给大数据分析推广适用,因此需要获取recycleView的有效曝光的埋点数据;
- 要求
- RecycleView中复用条目不用重复埋点,除非下拉刷新数据;
- 待确定:条目UI显示超过50%方可埋点,否则不埋点;
分析
由于RecycleView的四级缓存机制,当我们在onBinding中绑定数据时埋点会增加二级缓存的埋点,导致获取有效曝光不准确问题?如何解决该问题:两种方式
View绘制流程
- 平台测目前在用重写onAttachedToWindow()和onDetachedFromWindow()这两个方法在RecyclerView内部会在View移动出可视区域的时候被触发;
- 当 Adapter 创建的 View 在被滑动进屏幕的时onViewAttachedToWindow() 会直接回调,反之,在列表项 View 被窗口分离(即滑动离开了当前窗口界面的)的时onViewDetachedToWindow() 会立马被调用。
- 根据以上特性,在adapter中重写onViewAttachedToWindow(RecycleView.ViewHolder)可以获取当前列表刚刚滑进屏幕的条目布局信息,那么埋点的数据如何绑定?
- 重写viewHolder通过tag保存和读取,平台已经封装ViewHolder,需要修改每个Delegate中的viewHolder继承该类
public class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder { private SparseArray<View> mViews = new SparseArray(); private SparseArray<Object> mKeyedTags; public ViewHolder(View itemView) { super(itemView); } public void setTag(int key, Object tag) { if (key >>> 24 < 2) { throw new IllegalArgumentException("The key must be an application-specific resource id."); } else { if (this.mKeyedTags == null) { this.mKeyedTags = new SparseArray(2); } this.mKeyedTags.put(key, tag); } } public Object getTag(int key) { return this.mKeyedTags != null ? this.mKeyedTags.get(key) : null; }
- adapter通过Delegate添加每个条目布局和数据,然后在Delegate的 onBindViewHolder中设置tag属性,并将埋点所需的条目数据添加进去
public class PtClientAdapter extends JobPtAbsDelegationAdapter {
public PtClientAdapter(Activity activity, List<PtCateListBean.PtBaseListBean> items, OnOptCallBack onOptCallBack,
OnItemClickCallback onItemClickCallback,ActionUniteInterface callBack) {
this.delegatesManager.addDelegate(new PtClientNormalDelegate(activity , mCallBack)); //普通兼职职位
this.delegatesManager.addDelegate(new PtListBannersDelegate(activity , mCallBack));//轮播图
this.delegatesManager.addDelegate(new PtOnlineTaskDelegate(activity, onItemClickCallback , mCallBack));//线上任务
this.delegatesManager.addDelegate(new PtHotCateDelegate(activity, onFilterCallback , mCallBack)); //你可能在找
this.delegatesManager.addDelegate(new PtEncourageVideoDelegate(activity , mCallBack));//激励视频
this.delegatesManager.addDelegate(new PtResumeDelegate(activity , mCallBack)); //简历引导
this.delegatesManager.addDelegate(new PtCustomDelegate(activity , mCallBack)); //会员定制
this.delegatesManager.addDelegate(new PtOperatingItemDelegate(activity , mCallBack)); //猜你喜欢
}
}
//在Delegate中设置tag
public class PtClientNormalDelegate extends AdapterDelegate{
@Override
protected void onBindViewHolder(@NonNull List<PtCateListBean.PtBaseListBean> items, final int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
final PtCateListBean.PositionNormal positionNormalBean = (PtCateListBean.PositionNormal) items.get(position);
final NormalViewHolder viewHolder = (NormalViewHolder) holder;
//设置tag,并把当前条目信息加入缓存
viewHolder.setTag(R.id.id_tag_detail_bean, positionNormalBean);
}
}
- 由于每个Delegate对应的javaBean对象类都是不同,直接写到adapter中会导致无法很轻松理解,平台已经封装过了,在Adapter中通过DeleGateManager将onViewAttachedToWindow()分发给每一个Delegate类,因此可以直接重写Delegate的onViewAttachedToWindow(ViewHolder holder)
@Override
protected void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewHolder viewHolder = (ViewHolder) holder;
Object tag = viewHolder.getTag(R.id.id_tag_detail_bean);
if (tag instanceof PtCateListBean.PositionNormal) {
PtCateListBean.PositionNormal positionNormalBean = (PtCateListBean.PositionNormal) tag;
int adapterPosition = viewHolder.getAdapterPosition();
Log.e("shiq" , "当前被显示了 - onViewAttachedToWindow : " + positionNormalBean.title + " " + positionNormalBean + " ---- 列表中的位置为: " + adapterPosition);
}
}
- 以上对于每个Delegate都在自己类中添加有效埋点数据。便于后期维护,但有一个问题,PM要求相同的埋点滑动时只埋一次。onViewAttachedToWindow会每次显示均会调用一次。如何解决呢?
- 在javaBean中设置boolean值记录当前是否首次显示被埋点过了,如果埋点标记为true,后续显示均不会埋点了:优点简单,缺点如果javaBean是三方的不易修改;
- 在adapter中创建集合记录已经被标记埋点过的javaBean数据,如何区分javaBean唯一性,可以通过hashCode + position标记,如果首次显示埋点后添加记录,再次显示后过滤掉即可: 优点:不修改原有数据,缺点:每次都需判断是否在集合中,性能有所影响;
- 不足之处: onViewAttachedToWindow无法区分当前UI是否被显示超过50%;
通过RecycleView的滑动监听
- 通过监听RecycleView的滑动事件,获取当前屏幕显示的条目信息,根据条件删选即可!
- 重写RecycleView的onScrollStateChanged,onScrolled方法
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
// case RecyclerView.SCROLL_STATE_DRAGGING:
// case RecyclerView.SCROLL_STATE_SETTLING:
findScreenVisibleViewsAndNotify();
break;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx == 0 && dy == 0) { //如果当前是首次进入时设置
findScreenVisibleViewsAndNotify();
}
}
- RecycleVeiw的LinearLayoutManager布局获取当前屏幕显示的首位和末位的条目,不足: 结果并不准确,findLastVisibleItemPosition大于当前显示位置;
range[0] = manager.findFirstVisibleItemPosition();
range[1] = manager.findLastVisibleItemPosition();
-
对于上述中的不足之处,我们应该如何优化使之适合我们的要求,这里用到了view.getGlobalVisibleRect()获取的是view可见区域相对与屏幕来说的坐标位置;
Rect rect = new Rect();
boolean cover = view.getGlobalVisibleRect(rect);
//item逻辑上可见:可见且可见高度(宽度)>view高度(宽度)50%才行
boolean visibleHeightEnough = orientation == OrientationHelper.VERTICAL && rect.height() > view.getMeasuredHeight() / 2;
boolean visibleWidthEnough = orientation == OrientationHelper.HORIZONTAL && rect.width() > view.getMeasuredWidth() / 2;
boolean isItemViewVisibleInLogic = visibleHeightEnough || visibleWidthEnough;
if (cover && isItemViewVisibleInLogic) {
//去重,可埋点的数据
}
- 我们已经获取到了当前坐标position是否被显示且满足条件,对于去重,依然采用View绘制中两种方式,这里使用第二种,通过集合保存已被埋点数据,定义统一接口给adapter适配用于数据获取;
//数据区分接口
public interface IRecyclerViewAdapter {
/**
* 根据position获取item的数据
*/
Object getCurrentItemData(int position);
int getCurrentSize();
}
// 获取到需展示数据接口
public interface OnRecycleExposureListener {
/**
* 当前被展示的数据集合
* @param exposureBeans
*/
void onExposure(List<ExposureBean> exposureBeans);
}
Object itemData = null;
if (cover && isItemViewVisibleInLogic) {
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null && adapter instanceof IRecyclerViewAdapter){
int currentSize = ((IRecyclerViewAdapter) adapter).getCurrentSize();
if (currentSize > position){
itemData = ((IRecyclerViewAdapter) adapter).getCurrentItemData(position);
}
}
}
if (itemData == null) return;//如果不存在数据,跳过本次循环
if (mManager.addResource(mRule.createItemID(itemData, position))) {
mAllShowList.add(new ExposureBean(itemData, view, position));
}
- 提供通用的去重规则接口,便于后续扩展,这里使用规则为javaBean的hanshCode + position
- 数据管理集合,由于每次均需要查询是否在其中,这里为了效率mManager推荐使用hashSet,尽量避免使用ArrayList,当列表数据过大时会影响效率!
- 获取到的数据保存在mAllShowList集合中,通过接口回掉或者动态代理(如果不太清楚adapter类型,在view层通过 instanceof OnRecycleExposureListener)
- 在adapter中实现接口,分发给每个Delegate去埋点,也可以通过view.setTag和getTag使用获取;
public void onExposure(List<ExposureBean> exposureBeans) {
if (exposureBeans != null && !exposureBeans.isEmpty()) {
for (ExposureBean bean : exposureBeans) {
//根据当前位置获取设置AdapterDelegate
int itemViewType = this.delegatesManager.getItemViewType(items, bean.position);
AdapterDelegate delegateForViewType = this.delegatesManager.getDelegateForViewType(itemViewType);
if (bean.itemData != null && delegateForViewType != null)
delegateForViewType.exPostActionItem(bean.itemData, bean.position);
}
}
}
- 总结: RecycleView的adapter实现IRecyclerViewAdapter提供去重数据,OnRecycleExposureListener返回需要埋点集合,每个Delegate重写exPostActionItem方法去添加有效曝光的埋点即可!