RecycleView的有效埋点问题

问题

PM需要获取当前条目的有效曝光给大数据分析推广适用,因此需要获取recycleView的有效曝光的埋点数据;

  • 要求
    1. RecycleView中复用条目不用重复埋点,除非下拉刷新数据;
    2. 待确定:条目UI显示超过50%方可埋点,否则不埋点;

分析

由于RecycleView的四级缓存机制,当我们在onBinding中绑定数据时埋点会增加二级缓存的埋点,导致获取有效曝光不准确问题?如何解决该问题:两种方式

View绘制流程
  • 平台测目前在用重写onAttachedToWindow()和onDetachedFromWindow()这两个方法在RecyclerView内部会在View移动出可视区域的时候被触发;
    1. 当 Adapter 创建的 View 在被滑动进屏幕的时onViewAttachedToWindow() 会直接回调,反之,在列表项 View 被窗口分离(即滑动离开了当前窗口界面的)的时onViewDetachedToWindow() 会立马被调用。
  1. 根据以上特性,在adapter中重写onViewAttachedToWindow(RecycleView.ViewHolder)可以获取当前列表刚刚滑进屏幕的条目布局信息,那么埋点的数据如何绑定?
    • 重写viewHolder通过tag保存和读取,平台已经封装ViewHolder,需要修改每个Delegate中的viewHolder继承该类
    public class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder {
     private SparseArray mViews = new SparseArray();
     private SparseArray 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;
     }
    
    
    1. adapter通过Delegate添加每个条目布局和数据,然后在Delegate的 onBindViewHolder中设置tag属性,并将埋点所需的条目数据添加进去
    public class PtClientAdapter extends JobPtAbsDelegationAdapter {
        public PtClientAdapter(Activity activity, List 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 items, final int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List 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);
        }
    }
    
    
    1. 由于每个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会每次显示均会调用一次。如何解决呢?
      1. 在javaBean中设置boolean值记录当前是否首次显示被埋点过了,如果埋点标记为true,后续显示均不会埋点了:优点简单,缺点如果javaBean是三方的不易修改;
      2. 在adapter中创建集合记录已经被标记埋点过的javaBean数据,如何区分javaBean唯一性,可以通过hashCode + position标记,如果首次显示埋点后添加记录,再次显示后过滤掉即可: 优点:不修改原有数据,缺点:每次都需判断是否在集合中,性能有所影响;
    • 不足之处: onViewAttachedToWindow无法区分当前UI是否被显示超过50%;

    通过RecycleView的滑动监听

    • 通过监听RecycleView的滑动事件,获取当前屏幕显示的条目信息,根据条件删选即可!
    1. 重写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();
            }
        }
    
    1. RecycleVeiw的LinearLayoutManager布局获取当前屏幕显示的首位和末位的条目,不足: 结果并不准确,findLastVisibleItemPosition大于当前显示位置;
     range[0] = manager.findFirstVisibleItemPosition();
     range[1] = manager.findLastVisibleItemPosition();
    
    1. 对于上述中的不足之处,我们应该如何优化使之适合我们的要求,这里用到了view.getGlobalVisibleRect()获取的是view可见区域相对与屏幕来说的坐标位置;


      image
     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) {
        //去重,可埋点的数据
    }
    
    1. 我们已经获取到了当前坐标position是否被显示且满足条件,对于去重,依然采用View绘制中两种方式,这里使用第二种,通过集合保存已被埋点数据,定义统一接口给adapter适配用于数据获取;
    //数据区分接口
    public interface IRecyclerViewAdapter {
        /**
         * 根据position获取item的数据
         */
        Object getCurrentItemData(int position);
    
        int getCurrentSize();
    
    }
    // 获取到需展示数据接口
    public interface OnRecycleExposureListener {
    
        /**
         * 当前被展示的数据集合
         * @param exposureBeans
         */
        void onExposure(List 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)
    1. 在adapter中实现接口,分发给每个Delegate去埋点,也可以通过view.setTag和getTag使用获取;
    public void onExposure(List 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方法去添加有效曝光的埋点即可!

    你可能感兴趣的:(RecycleView的有效埋点问题)