曝光埋點方案: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%:上報可見

onScrollStateChangedSCROLL_STATE_IDLE

FLING

onScrollStateChangedSCROLL_STATE_FLINNG

上報時機就對應recyclerView的滾動監聽的兩個方法,onScrollStateChanged、onScrolled。

列表item曝光邏輯

item的曝光:下一次上報item時,看上次上報可見的 是否不可見了。

title“more”的曝光:根據模塊可見就上報可見,模塊不可見就上報不可見

無橫(豎)滑的模塊 的子view,根據模塊可見性 全部子view都上報相同的可見性。

有橫(豎)滑的模塊 的子view:若模塊可見,就上報 當前子列表中 的可見子模塊

同時處理子列表滑動時的item可見性;模塊不可見,那當前子列表的可見view上報不可見。

1、item上報可見時,如果已經之前上報可見了,就不上報;上報不可見時,如果上次上報了可見,才上報。

2、模塊標題的曝光就是模塊的曝光

3、item內的元素是 不可滑動/可滑動列表,是不同處理方式。其中元素是不可滑動時處理得比較粗糙,可以再優化下。

概念說明

1、邏輯可見:可見寬/>50% 

2、視覺可見:模塊視覺上可見,無論看見多少。

說明:本文說的 寬高>50%、可見都是 邏輯可見

三、曝光邏輯代碼說明

預備知識:view可見性的判斷

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;
    }
}

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章