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