目前贝壳找房 APP 端的曝光时机是写死的, 触发条件:卡片必须要完整展示在界面上; 在列表界面上下/左右滑动时单次/多次曝光同一个卡片。
现有方案的不足:
1.门限条件应改为 API 下发的;
2.缺少卡片在界面上显示的时长;
反例:
1.比如说列表有 1000 条记录,快速滑动列表到最后一条;用户并没有看清中间的 900 多条记录,这时要不要为这些记录做曝光埋点?
2.例如一个卡片高度为 100px,实际上只显示了 80px,是否要做一次曝光埋点。
当前问题:
如果只滑动这个程度,目前 app 不会为“附近地图”做曝光埋点,但该卡片的主要信息都已经展示了
行业对标:
今日头条、手机百度的曝光埋点策略做的很细, 比如卡片划入、划出时间,卡片显示多少比例可以算曝光等等。
解决方案
参考今日头条、手机百度的做法,实现类似的曝光策略。
1.为每种卡片设置不同的曝光策略;
2.APP 根据 API 下发的门限条件触发埋点;
3.记录卡片移入、移出屏幕的时间, 统计每个卡片真正显示的时长;
4.界面销毁、显示/隐藏是否触发曝光埋点。例如按 home 键时是否触发曝光埋点,再次进入是否触发埋点。 这些场景由 API 下发配置开关。
双向队列缓存当前 RecyclerView 显示的所有 ViewHolder, 用于执行卡片的曝光埋点函数。
在监听 RecyclerView 滑动事件时得到第一个可见位置、最后一个可见位置,根据参数判断是上滑或下滑,通过判断 ViewHolder 的 itemView top、bottom 参数值得出刚刚移入屏幕的卡片显示比例, 并根据 API 下发的门限值(最低显示比例)记录开始时间,在卡片即将划出屏幕时(API 下发的门限值)触发曝光埋点。 从而得出卡片的显示周期。
参考代码
滑动回调
public class CardExposureHelper extends RecyclerView.OnScrollListener {
//缓存卡片的双向队列
private Deque<BaseHomeCard> deque;
//队列顶部Card的position
private int preFirstExposure;
//队列底部Card的position
private int preLastExposure;
/**
* 处理垂直方向卡片曝光
* @param manager
* @param isUp 是否向上滑动
*/
private void onVerticalExposure(LinearLayoutManager manager,boolean isUp) {
int firstVisiblePosition = manager.findFirstVisibleItemPosition();
int lastVisiblePosition = manager.findLastVisibleItemPosition();
//根据曝光比例判断第一个可见卡片是否需要曝光
firstVisiblePosition = isVerticalExposure(firstVisiblePosition)?firstVisiblePosition:firstVisiblePosition+1;
//根据曝光比例判断最后一个可见卡片是否需要曝光
lastVisiblePosition = isVerticalExposure(lastVisiblePosition)?lastVisiblePosition:lastVisiblePosition-1;
//第一次曝光,曝光所有符合曝光比例的Card
if (preFirstExposure==0&&preLastExposure==0){
offerVerticalVisibleQueue(firstVisiblePosition,lastVisiblePosition,true);
}else if (isUp){
//向上滑动,把顶部不可见Card从顶部出队,底部进入可曝光的卡片入队
popVerticalVisibleQueue(preFirstExposure,firstVisiblePosition-1,true);
offerVerticalVisibleQueue(preLastExposure+1,lastVisiblePosition,false);
}else {
//对应向下滑动的策略
popVerticalVisibleQueue(lastVisiblePosition+1,preLastExposure,false);
offerVerticalVisibleQueue(firstVisiblePosition,preFirstExposure-1,true);
}
//更新队列的顶部position和底部position
preFirstExposure = firstVisiblePosition;
preLastExposure = lastVisiblePosition;
}
/**
* 入队操作
* @param start
* @param end
* @param isFirst 是否从顶部入队
*/
private void offerVerticalVisibleQueue(int start,int end,boolean isFirst){
if (start>=0 && end<recyclerView.getAdapter().getItemCount() && start<=end){
if (isFirst){
for (int i=end;i>=start;i--){
onVerticalItemSlideInto(i,true);
}
}else {
for (int i=start;i<=end;i++){
onVerticalItemSlideInto(i,false);
}
}
}
}
/**
* 出队操作
* @param start
* @param end
* @param isFirst 是否从顶部出队
*/
private void popVerticalVisibleQueue(int start,int end,boolean isFirst){
if (start>=0 && end<recyclerView.getAdapter().getItemCount() && start<=end){
if (isFirst){
for (int i=start;i<=end;i++){
onVerticalItemSlideOut(i,isFirst);
}
}else {
for (int i=end;i>=start;i--){
onVerticalItemSlideOut(i,isFirst);
}
}
}
}
/**
* 处理滑入(入队)可曝光的卡片
* @param position
* @param isFirst 是否从顶部滑入(入队)
*/
private void onVerticalItemSlideInto(int position,boolean isFirst){
BaseHomeCard card = getBaseHomeCard(position);
if (isFirst){
deque.offerFirst(card);
}else {
deque.offerLast(card);
}
//回调卡片开始曝光事件
callItemExposure(card,position);
}
/**
* 处理滑出(出队)停止曝光的卡片
* @param position
* @param isFirst 是否从顶部滑出(出队)
*/
private void onVerticalItemSlideOut(int position,boolean isFirst){
BaseHomeCard card;
if (isFirst){
card = deque.removeFirst();
}else {
card = deque.removeLast();
}
//回调卡片结束曝光事件
callItemEndExposure(card,position,isFirst);
}
复制代码
展望
目前这是 APP 端做的技术储备, 如需上线仍需要产品经理做更细致的产品规划。
作者介绍:
高瑞 、贺宇成,Android 工程师,负责贝壳找房 app 安卓端研发工作。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/TKgFlupncu-Fol-mH8OEYA
评论