曝光埋点方案:recyclerView中的item曝光逻辑实现

时间:2022-07-22
本文章向大家介绍曝光埋点方案:recyclerView中的item曝光逻辑实现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

电商app的首页,一般是可滑动列表,当用户上下滑动时,列表中的item可能会多次出现在屏幕上。某个item从出现到消失的过程大于某一时间(比如1s),就认为是一次曝光。数据分析同事对这些曝光数据的分析,可用于针对用户进行商品喜好的推荐。

那如何实现 列表(recyclerView)中item的曝光埋点呢?

一、曝光埋点 的问题点

首先,客户端要考虑的就是只管调用api上报:上报item可见、上报item不可见。至于是否是有效曝光,就是公共埋点SDK(中台提供)去计算了。

所以本文重点就是,滑动recyclerView时 item变为可见、变为不可见,什么时候、怎么样 上报

二、曝光逻辑分析

如下淘宝首页,竖向列表中有很多模块item,聚划算、天天特卖、猜你喜欢等等。而每个模块内部又有多个子item,比如:可横向滑动的菜单模块内有两排菜单、聚划算内展示了两个商品。

这里先列出实现逻辑。

上报时机

回调实现

刚进入页面时(可见且>50%:上报可见)

第一次onScroll

手指拖动滑动时( 不停的:不可见或<50%:上报消失、可见且>50%:上报可见

onScroll、且SCROLL_STATE_TOUCH_SCROLL

滑动停止时( <50%(之前上报过可见):上报消失;可见且>50%:上报可见 )

onScrollStateChanged、SCROLL_STATE_IDLE

FLING时

onScrollStateChanged、SCROLL_STATE_FLINNG

上报时机就对应recyclerView的滚动监听的两个方法,onScrollStateChanged、onScrolled。

列表item曝光逻辑

item的曝光:下一次上报item时,看上次上报可见的 是否不可见了。

title“more”的曝光:根据模块可见就上报可见,模块不可见就上报不可见|

无横(竖)滑的模块 的子view,根据模块可见性 全部子view都上报相同的可见性。

有横(竖)滑的模块 的子view:若模块可见,就上报 当前子列表中 的可见子模块 ;同时处理子列表滑动时的item可见性;模块不可见,那当前子列表的可见view上报不可见。

  • item上报可见时,如果已经之前上报可见了,就不上报;上报不可见时,如果上次上报了可见,才上报。
  • 模块标题的曝光就是模块的曝光
  • item内的元素是 不可滑动/可滑动列表,是不同处理方式。其中元素是不可滑动时处理得比较粗糙,可以再优化下。

概念说明:

  1. 逻辑可见:可见宽/高>50%
  2. 视觉可见:模块视觉上可见,无论看见多少。

说明:本文说的 宽高>50%、可见都是 逻辑可见。

三、曝光逻辑代码说明

预备知识,view可见性的判断,https://www.jianshu.com/p/30b0ae304518

1、对recyclerView的滚动监听

滚动监听的目的:滑动中item是可能多次曝光的,在列表 静止、手指拖动、快速滑动时都要 监听item的可见性,然后把可见或不可见回调,然后根据position具体上报item信息。

    /**
     * 设置RecyclerView的item可见状态的监听
     * @param recyclerView recyclerView
     * @param onExposeListener 列表中的item可见性的回调
     */
    public void setRecyclerItemExposeListener(RecyclerView recyclerView, OnItemExposeListener onExposeListener) {

        mItemOnExposeListener = onExposeListener;
        mRecyclerView = recyclerView;

        if (mRecyclerView == null || mRecyclerView.getVisibility() != View.VISIBLE) {
            return;
        }
        //检测recyclerView的滚动事件
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                //关注:SCROLL_STATE_IDLE:停止滚动;  SCROLL_STATE_DRAGGING: 用户慢慢拖动
                // 关注:SCROLL_STATE_SETTLING:惯性滚动
                if (newState == RecyclerView.SCROLL_STATE_IDLE
                        || newState == RecyclerView.SCROLL_STATE_DRAGGING
                        || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    handleCurrentVisibleItems();
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                //包括刚进入列表时统计当前屏幕可见views
                handleCurrentVisibleItems();
            }

        });
    }

2、具体的处理逻辑在handleCurrentVisibleItems中,主要两点:1,判断recyclerView视觉可见,2、获取此时recyclerView中 第一个、最后一个 视觉可见item的position。

/**
     * 处理 当前屏幕上mRecyclerView可见的item view
     */
    public void handleCurrentVisibleItems() {
        //1、View.getGlobalVisibleRect(new Rect()),true表示view视觉可见,无论可见多少。
        if (mRecyclerView == null || mRecyclerView.getVisibility() != View.VISIBLE ||
                !mRecyclerView.isShown() || !mRecyclerView.getGlobalVisibleRect(new Rect())) {
            return;
        }
        //保险起见,为了不让统计影响正常业务,这里做下try-catch
        try {
            int[] range = new int[2];
            int orientation = -1;
            RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
            if (manager instanceof LinearLayoutManager) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager;
                range = findRangeLinear(linearLayoutManager);
                orientation = linearLayoutManager.getOrientation();
            } else if (manager instanceof GridLayoutManager) {
                GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
                range = findRangeGrid(gridLayoutManager);
                orientation = gridLayoutManager.getOrientation();
            } else if (manager instanceof StaggeredGridLayoutManager) {
                StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) manager;
                range = findRangeStaggeredGrid(staggeredGridLayoutManager);
                orientation = staggeredGridLayoutManager.getOrientation();
            }
            if (range == null || range.length < 2) {
                return;
            }
            XLogUtil.d("屏幕内可见条目的起始位置:" + range[0] + "---" + range[1]);
            //2 注意,这里 会处理此刻 滑动过程中 所有可见的view
            for (int i = range[0]; i <= range[1]; i++) {
                View view = manager.findViewByPosition(i);
                setCallbackForLogicVisibleView(view, i, orientation);
            }
        } catch (Exception e) {
            e.printStackTrace();
            XLogUtil.d(e.getMessage());
        }
    }

3、然后交给setCallbackForLogicVisibleView处理每个视觉可见position,就是判断是否是 逻辑可见(宽或高大于50%),然后回调出去。注意,这里回调出去的的逻辑可见、逻辑不可见,都是 在视觉可见的基础上 判断 宽或高是否大于50% 。

/**
     * 为 逻辑上可见的view设置 可见性回调
     * 说明:逻辑上可见--可见且可见高度(宽度)>view高度(宽度)的50%
     * @param view 可见item的view
     * @param position 可见item的position
     * @param orientation recyclerView的方向
     */
    private void setCallbackForLogicVisibleView(View view, int position, int orientation) {
        if (view == null || view.getVisibility() != View.VISIBLE ||
                !view.isShown() || !view.getGlobalVisibleRect(new Rect())) {
            return;
        }

        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 && mIsRecyclerViewVisibleInLogic && isItemViewVisibleInLogic) {
            mItemOnExposeListener.onItemViewVisible(true, position);
        }else {
            mItemOnExposeListener.onItemViewVisible(false, position);
        }
    }

4、以上就是recyclerView item曝光的完整逻辑了。 如果item内部 是 可滑动的recyclerView,那么就item可见时 子列表也做滚定监听就可以了,即内部的recyclerView也是用setRecyclerItemExposeListener。

建议,调用setRecyclerItemExposeListener给recyclerView设置曝光监听的listener直接传adapter,在adapter实现回调方法,然后就可以根据回调的position调用埋点 sdk的可见、不可见api上报信息了。

5、完整代码如下

曝光监听接口:

public interface OnItemExposeListener {

    /**
     * item 可见性回调
     * 回调此方法时 视觉上一定是可见的(无论可见多少)
     * @param visible true,逻辑上可见,即宽/高 >50%
     * @param position item在列表中的位置
     */
    void onItemViewVisible(boolean visible, int position);

}

曝光(可见性) 监听工具,主要方法setRecyclerItemExposeListener:

public class HomePageExposeUtil {

    private OnItemExposeListener mItemOnExposeListener;

    /**
     * 列表是否逻辑上可见
     *
     * 默认true:意思是 RecyclerView的可见性没有外部逻辑的判断
     * false:例如,人气商品模块,横滑的商品RecyclerView,逻辑上是 人气商品模块 出现一半 时 商品RecyclerView才算可见。
     *          所以一开始设置为false,人气商品模块 出现 大于一半时,设置为true。
     */
    private boolean mIsRecyclerViewVisibleInLogic = true;

    private RecyclerView mRecyclerView;

    /**
     * 一般使用这个即可
     */
    public HomePageExposeUtil() {
        mIsRecyclerViewVisibleInLogic = true;
    }

    /**
     * 当RecyclerView本身的可见性 受外部逻辑控制时 使用,
     * @param isRecyclerViewVisibleInLogic
     */
    public HomePageExposeUtil(boolean isRecyclerViewVisibleInLogic) {
        mIsRecyclerViewVisibleInLogic = isRecyclerViewVisibleInLogic;
    }


    /**
     * 设置RecyclerView的item可见状态的监听
     * @param recyclerView recyclerView
     * @param onExposeListener 列表中的item可见性的回调
     */
    public void setRecyclerItemExposeListener(RecyclerView recyclerView, OnItemExposeListener onExposeListener) {

        mItemOnExposeListener = onExposeListener;
        mRecyclerView = recyclerView;

        if (mRecyclerView == null || mRecyclerView.getVisibility() != View.VISIBLE) {
            return;
        }
        //检测recyclerView的滚动事件
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                //关注:SCROLL_STATE_IDLE:停止滚动;  SCROLL_STATE_DRAGGING: 用户慢慢拖动
                // 关注:SCROLL_STATE_SETTLING:惯性滚动
                if (newState == RecyclerView.SCROLL_STATE_IDLE
                        || newState == RecyclerView.SCROLL_STATE_DRAGGING
                        || newState == RecyclerView.SCROLL_STATE_SETTLING) {
                    handleCurrentVisibleItems();
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                //包括刚进入列表时统计当前屏幕可见views
                handleCurrentVisibleItems();
            }

        });
    }

    /**
     * 处理 当前屏幕上mRecyclerView可见的item view
     */
    public void handleCurrentVisibleItems() {
        //View.getGlobalVisibleRect(new Rect()),true表示view视觉可见,无论可见多少。
        if (mRecyclerView == null || mRecyclerView.getVisibility() != View.VISIBLE ||
                !mRecyclerView.isShown() || !mRecyclerView.getGlobalVisibleRect(new Rect())) {
            return;
        }
        //保险起见,为了不让统计影响正常业务,这里做下try-catch
        try {
            int[] range = new int[2];
            int orientation = -1;
            RecyclerView.LayoutManager manager = mRecyclerView.getLayoutManager();
            if (manager instanceof LinearLayoutManager) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager;
                range = findRangeLinear(linearLayoutManager);
                orientation = linearLayoutManager.getOrientation();
            } else if (manager instanceof GridLayoutManager) {
                GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
                range = findRangeGrid(gridLayoutManager);
                orientation = gridLayoutManager.getOrientation();
            } else if (manager instanceof StaggeredGridLayoutManager) {
                StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) manager;
                range = findRangeStaggeredGrid(staggeredGridLayoutManager);
                orientation = staggeredGridLayoutManager.getOrientation();
            }
            if (range == null || range.length < 2) {
                return;
            }
            XLogUtil.d("屏幕内可见条目的起始位置:" + range[0] + "---" + range[1]);
            // 注意,这里 会处理此刻 滑动过程中 所有可见的view
            for (int i = range[0]; i <= range[1]; i++) {
                View view = manager.findViewByPosition(i);
                setCallbackForLogicVisibleView(view, i, orientation);
            }
        } catch (Exception e) {
            e.printStackTrace();
            XLogUtil.d(e.getMessage());
        }
    }

    /**
     * 为 逻辑上可见的view设置 可见性回调
     * 说明:逻辑上可见--可见且可见高度(宽度)>view高度(宽度)的50%
     * @param view 可见item的view
     * @param position 可见item的position
     * @param orientation recyclerView的方向
     */
    private void setCallbackForLogicVisibleView(View view, int position, int orientation) {
        if (view == null || view.getVisibility() != View.VISIBLE ||
                !view.isShown() || !view.getGlobalVisibleRect(new Rect())) {
            return;
        }

        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 && mIsRecyclerViewVisibleInLogic && isItemViewVisibleInLogic) {
            mItemOnExposeListener.onItemViewVisible(true, position);
        }else {
            mItemOnExposeListener.onItemViewVisible(false, position);
        }
    }


    private int[] findRangeLinear(LinearLayoutManager manager) {
        int[] range = new int[2];
        range[0] = manager.findFirstVisibleItemPosition();
        range[1] = manager.findLastVisibleItemPosition();
        return range;
    }

    private int[] findRangeGrid(GridLayoutManager manager) {
        int[] range = new int[2];
        range[0] = manager.findFirstVisibleItemPosition();
        range[1] = manager.findLastVisibleItemPosition();
        return range;

    }

    private int[] findRangeStaggeredGrid(StaggeredGridLayoutManager manager) {
        int[] startPos = new int[manager.getSpanCount()];
        int[] endPos = new int[manager.getSpanCount()];
        manager.findFirstVisibleItemPositions(startPos);
        manager.findLastVisibleItemPositions(endPos);
        int[] range = findRange(startPos, endPos);
        return range;
    }

    private int[] findRange(int[] startPos, int[] endPos) {
        int start = startPos[0];
        int end = endPos[0];
        for (int i = 1; i < startPos.length; i++) {
            if (start > startPos[i]) {
                start = startPos[i];
            }
        }
        for (int i = 1; i < endPos.length; i++) {
            if (end < endPos[i]) {
                end = endPos[i];
            }
        }
        int[] res = new int[]{start, end};
        return res;
    }


    public void setIsRecyclerViewVisibleInLogic(boolean mIsRecyclerViewVisibleInLogic) {
        this.mIsRecyclerViewVisibleInLogic = mIsRecyclerViewVisibleInLogic;
    }
}