分两种场景考虑:
场景一
描述:父View是一个RecyclerView,记录其子View被浏览次次数
思路:
实现:
1、监听recylerview的滚动事件
public class ViewShowCountUtils { //刚进入列表时统计当前屏幕可见views private boolean isFirstVisible = true; //用于统计曝光量的map private Map countMap = new HashMap(); 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) { 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 countMap = new HashMap<>(); private List 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(); 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 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 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标志位控制