记录View的曝光(被浏览)次数

分两种场景考虑:

场景一

描述:父View是一个RecyclerView,记录其子View被浏览次次数

 

思路:

 实现:

1. 监听recylerview的滚动事件

public class ViewShowCountUtils {

     //刚进入列表时统计当前屏幕可见views
    private boolean isFirstVisible = true;

    //用于统计曝光量的map
    private Map<String, Integer> countMap = new HashMap<String, Integer>();
       
    void recordViewShowCount(RecyclerView recyclerView){
        hashMap.clear();
        if (recyclerView == null || recyclerView.getVisibility() != View.VISIBLE) {
            return;
        }

        recyclerView.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) {
                    //停止滚动,记录当前曝光的view
                    getVisibleViews(recyclerView);
                }

            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                //...初次show,该方法也会回调,这里通过设立标志位判断是否是first show,是的话则记录一次
                if (isFirstVisible) {
                    getVisibleViews(recyclerView);
                    isFirstVisible = false;
                }

            }
        });

2. 获取可见item view的位置

recylerview的manager提供了对应的方法。 findFirstVisibleItemPosition()和findLastVisibleItemPosition()可获取可见的item view的位置

        int[] range = new int[2];
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            range = findRangeLinear((LinearLayoutManager) manager);
        } else if (manager instanceof GridLayoutManager) {
            range = findRangeGrid((GridLayoutManager) manager);
        } else if (manager instanceof StaggeredGridLayoutManager) {
            range = findRangeStaggeredGrid((StaggeredGridLayoutManager) manager);
        }

LinearLayoutManager和GridLayoutManager方式相同 


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

StaggeredGridLayoutManager获取方式复杂一些

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

 3. 根据可见item view的位置,按需要,记录对应的View & view的数据

遍历range,计算每个可见item是否符合曝光条件(这里是显示高度必须大于自1/2),符合条件才统计数据

     for (int i = range[0]; i <= range[1]; i++) {
            recordViewCount(manager.findViewByPosition(i), i);
        }

    private void recordViewCount(View view, int pos) {
        if (view == null || view.getVisibility() != View.VISIBLE ||
                !view.isShown() || !view.getGlobalVisibleRect(new Rect())) {
            return null;
        }
        int top = view.getTop();
        int halfHeight = view.getHeight() / 2;

        int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
        int statusBarHeight = getStatusBarHeight(view.getContext());
        int searchBarHeight = binding.searchBar.getHeight(); // recyclerView顶部view的高度

        if (top < 0 && Math.abs(top) > halfHeight) {
            return null;
        }
        if (top > screenHeight - halfHeight - statusBarHeight - searchBarHeight) {
            return null;
        }
        //注意:获取view绑定的数据作为该Item view的key,必须在RecyclerView相应adapter中setTag(onBindViewHolder给item绑定数据的时候)
        String tagKey = (String) view.getTag();
        if (TextUtils.isEmpty(tagKey)) {
            return null;
        }
        countMap.put(tagKey, !countMap.containsKey(tagKey) ? 1 : ((Integer) countMap.get(tagKey) + 1));
    }

  
    }

整合以上步骤可以把它写成一个工具类,在RecyclerView设置数据刷新的时候使用,参考

在这里

场景二

描述: 父view是一个滑动控件,具有多个同级recycleView,需要记录每个RecyclerView的item 曝光次数

 思路:

此时,不能分别对每个RecyclerView像场景一那样进行滑动监听。因为,这里的滑动事件被NestedScrollView消耗

这里判断 & 记录View的曝光和场景一的方式相同

class MyFragment {
    //记录View的曝光,key-View对应的tag,value-曝光次数
    private Map<String, Integer> countMap = new HashMap<>();

    private List<ImpressedData> getVisibleViews(RecyclerView recyclerView) {
        if (recyclerView == null || recyclerView.getVisibility() != View.VISIBLE ||
                !recyclerView.isShown() || !recyclerView.getGlobalVisibleRect(new Rect())) {
            return null;
        }
        int[] range = new int[2];
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            range = findRangeLinear((LinearLayoutManager) manager);
        } else if (manager instanceof GridLayoutManager) {
            range = findRangeGrid((GridLayoutManager) manager);
        }
        if (range == null || range.length < 2) {
            return null;
        }

        List impressedDataList = new ArrayList<ImpressedData>();
        for (int i = range[0]; i <= range[1]; i++) {
            ImpressedData data = recordViewCount(manager.findViewByPosition(i), i);
            if (data != null) {
                impressedDataList.add(data);
            }
        }
        return impressedDataList;
    }

    private ImpressedData recordViewCount(View view, int pos) {
        if (view == null || view.getVisibility() != View.VISIBLE ||
                !view.isShown() || !view.getGlobalVisibleRect(new Rect())) {
            return null;
        }
        int top = view.getTop();
        int halfHeight = view.getHeight() / 2;

        int screenHeight = getDisplayHeight();
        int statusBarHeight = getStatusBarHeight(view.getContext());
        int actionBarHeight = binding.actionBar.getHeight();//NestedScrollView顶部的View, 如果没有,请忽略

        if (top < 0 && Math.abs(top) > halfHeight) {
            return null;
        }
        if (top > screenHeight - halfHeight - statusBarHeight - searchBarHeight) {
            return null;
        }

        String tagKey = (String) view.getTag();
        if (TextUtils.isEmpty(tagKey)) {
            return null;
        }
        int count = !countMap.containsKey(tagKey) ? 1 : ((Integer) countMap.get(tagKey) + 1);
        countMap.put(tagKey, count);
        return new ImpressedData(pos, count);
    }

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

   // 屏幕高度(不包含底部隐形导航栏的高度)
    public int getDisplayHeight() {
        DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
        return displayMetrics.heightPixels;
    }

    // 获取状态栏的高度
    public static int getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }
    

}

其中存储曝光item view 的位置(在当前RecyclerView的位置)和曝光次数的类是

data class ImpressedData(val pos: Int, val count: Int)

然后, 记录滑动过程的View曝光

    private PublishProcessor<Integer> scrollEvent;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        scrollEvent = PublishProcessor.create();
    }
    
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        setScrollChangedListener();
    }    

    //添加滑动监听,每0.5s发送最后一次滑动事件
    private void setScrollChangedListener() {
        binding.scrollView.setScrollViewListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> scrollEvent.onNext(scrollY));

        Disposable impressDisposable = scrollEvent.toObservable()
                .throttleLast(500, TimeUnit.MILLISECONDS)
                .subscribe(this::impressAllViews);
        compositeDisposable.add(impressDisposable);
    }

    // 检查nestedScrollView内的所有ReccyclerView的曝光情况
    private void impressAllViews(Integer scrollY) {
        sendViewImpressed(binding.recyclerView1.getAdapter(), getVisibleViews(binding.recyclerView1));
        sendViewImpressed(binding.recyclerView2.getAdapter(), getVisibleViews(binding.recyclerView2));
        sendViewImpressed(binding.recyclerView3.getAdapter(), getVisibleViews(binding.recyclerView3));
    }

    //RecyclerView如果有曝光的item view,则上报
    private void sendViewImpressed(RecyclerView.Adapter adapter, List<ImpressedData> impressedDataList) {
        if (adapter == null || impressedDataList == null || impressedDataList.isEmpty()) {
            return;
        }
        for (int i = 0; i < impressedDataList.size(); i++) {
            sendSearchImpress(adapter, impressedDataList.get(i));
        }
    }

    private void sendSearchImpress(RecyclerView.Adapter adapter, ImpressedData impressedData) {
        if (adapter instanceof adapter1) {
            ...
        } else if (adapter instanceof adapter2) {
            ...
        } else if (adapter instanceof adapter3) {
            ...
        }
    }

最后,记录 first show(即滑动前)的View曝光情况。每次数据刷新都需要记录一次,但这里不能在adapter更新数据(即notifyDataSetChanged)后立马判断,因为可能在RecyclerView刷新(即View重新布局、绘制)前就去执行曝光判断了,此时结果肯定是不准确的。这里对RecyclerView的ViewTree添加addOnPreDrawListener监听,在layout后draw之前进行曝光判断,此时item view的数据以及View的长宽都是准确的

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        ...
        setRecyclerPreDrawListener();
    }


    private void setRecyclerPreDrawListener() {
        binding.recyclerView1.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                // isInitView
                if (((Adapter1) binding.recyclerView1.getAdapter()).isInitView) {
                  //调用一次后需要注销这个监听,否则会阻塞ui线程  binding.recyclerView1.getViewTreeObserver().removeOnPreDrawListener(this);
                    ((SearchSectionAdapter) binding.recyclerView1.getAdapter()).isInitView = false;
                    sendViewImpressed(binding.recyclerView1.getAdapter(), getVisibleViews(binding.recyclerView1);
                }
                return true;
            }
        });
    }

注意:addOnPreDrawListener()在recycleView的item中使用时,即使使用removeOnPreDrawListener(this),但是onPreDraw()还是会被不断调用,阻塞ui线程,这个时候可以会用一个first标志位控制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值