分两种场景考虑:
场景一
描述:父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 PublishProcessorscrollEvent; @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标志位控制



