这篇文章是《聊实用功能设计系列》的第一期,聊一聊咱们线上我的项目信息流卡片曝光/生产的设计思路。
系列会拆分多期解说线上我的项目中重要性能的设计过程。侧重点在于让读者了解功能模块的设计意义及整体搭建思路,大部分细节实现各我的项目自行实现即可。
背景
市场上大部分 APP 都采纳了信息流的设计模式来承载信息内容展现,不同特色产品所设计的信息流模式不尽相同。
比方图片瀑布流,商品列表流或资讯Feed流等。
(淘宝商品流)
(懂车帝视频流)
只管展现形式不同,但内容在列表中的流向是统一的,也都以列表滑动的交互来曝光更多内容。
而列表内容的展现成果可通过统计列表项的曝光次数/点击次数/曝光时刻/滑动时刻等数据进行多维度计算掂量。
如常见的点击率,生产时长。
点击率=点击次数/曝光次数,点击率越高则示意用户对内容进一步理解的动向越高。
生产时长=t(滑动时刻)-t(上一次曝光时刻),生产时长越长则示意用户对内容趣味越高。
当然,不同列表内容的计算规定有所差别,考查内容展现成果的侧重点也不应雷同。
如资讯类 Feed 流中内容项更多是以图文混排的展现模式来吸引用户进行点击生产,更关注内容的点击率。
而对于视频流内短视频快消类内容项,个别反对在流上间接播放,更关注内容的观看生产时长。
所以信息流列表项的曝光信息/生产数据对于掂量流投放成果至关重要,那么如何获取这些信息呢?
设计思路
以咱们线上我的项目为例子。利用内信息流页面十分多。
有类微博的动静卡片流。
类资讯类卡片流。
双列视频流。
上述仅是 3 个常见场景,如果针对所有场景列表做设计,显然可维护性及扩展性十分差,所以必须思考做成通用性设计。
咱们约定几个高频词汇含意,后续形容都采纳其代替表述。
- 卡片,任意一个信息流列表项。
- 曝光,卡片的曝光行为。
- 生产,卡片的消费行为。
- 无效面积,卡片裸露在界面的可见面积。
- 无效面积率,卡片无效面积与卡片视图总面积的比率,个别可选 1/2 ~ 4/5。
- 无效卡片,卡片的无效面积率高于某个阈值时该卡片视为无效卡片,常见为 2/3,3/5。
- 最小无效生产时长,当无效卡片的生产时长小于该时长时则认为用户并无消费行为,个别可选再 100~200 毫秒。
无效面积是用于计算卡片在界面的可视大小,当某个卡片可视区域太小了,哪怕是长期停留,咱们都认为用户对其内容并无感知。
无效卡片则是用户对卡片内容有感知能力,这些卡片有机会失去曝光及生产。
有了上述约定,咱们再定义一种通用的卡片曝光/生产规定:
- 当列表静止且用户与界面无接触时,列表内的无效卡片记一次曝光行为;
- 已静止列表从新开始滑动时,原静止列表内的无效卡片记一次消费行为。
则依照规定,卡片一次曝光生产的逻辑如下:
- 在列表静止且用户与界面无接触时,收集可见的所有卡片;
- 过滤无效面积率小于 3/5 的卡片失去无效卡片集进行缓存,失去缓存集 list[以后无效卡片];
- 针对无效卡片集进行曝光并记录曝光时刻点 t[exposure],实际上这个工夫也是开始生产的工夫 t[startConsume]。
- 当列表开始滑动时,获取以后工夫戳减去上一次曝光工夫失去该次生产时长,若生产时长不短于 100毫秒,则list[以后无效卡片] 进行生产记录。
- 扩加强扩大。预留上述流程反对要害参数:比方是否动静调整无效面积率,最小无效生产时长等。
另外,还可管制是否同一个界面内同一个卡片是否容许屡次曝光,或管制卡片曝光的间隙等,按需实现即可。
具体实现
定义一个接口 CardLogFeed 形容卡片。
public interface CardLogFeed { //业务定义的卡片信息类,自定扩大批改 @Nullable CardLogInfo buildCardLogInfo(); //触发曝光回调 void logExposure(); //触发生产回调 void logConsume(long time); //是否是无效卡片 boolean isVaildLogUnit();}
每一个卡片曝光/生产的回调行为不尽相同,凋谢给业务方实现。
CardLogInfo 类记录每个卡片应上报的日志信息,个别状况下曝光/生产都须要上报这些信息。
isVaildLogUnit 办法要求业务方告知以后卡片是否为无效卡片。该办法实现了设计思路章节波及的无效卡片的断定逻辑。
另外,因为不同卡片款式上可能存在差别,如长方形,正方形,圆形,甚至不规则图形,实现思路并不局限在比拟可视面积占比上,业务实现方自由选择。
定义一个类CradLogScrollLisener类,继承 RecyclerView.OnScrollListener 并实现列表相干的逻辑。
首先,重载 onScrollStateChanged 办法获取列表状态并依据不同状态进行解决。
//记录以后列表状态private int currentNewState = SCROLL_STATE_IDLE;@Overridepublic void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); switch (newState) { //尝试曝光 case SCROLL_STATE_IDLE: { exposureCardLog(); break; } //尝试生产 case SCROLL_STATE_DRAGGING: { if (currentNewState != SCROLL_STATE_SETTLING) { consumeCardLog(startConsumeTime); } break; } default: break; } currentNewState = newState;}
通过捕捉SCROLL_STATE_IDLE状态尝试曝光,exposureCardLog 办法内实现无效卡片的收集流程及曝光,上面为外围逻辑(只列外围逻辑)。
private void exposureCardLog() { //记录曝光时刻 startConsumeTime = System.nanoTime(); //收集无效卡片 currentValidVisibleFeeds.currentValidVisibleFeeds(); if (currentValidVisibleFeeds.isEmpty()) { return; } //上传曝光数据 for (CardLogFeed logFeed : currentValidVisibleFeeds) { logFeed.logExposure(); }}
最初一步把收集到的 CardLogFeed 对象(无效卡片)进行曝光,而这些收集的逻辑在 currentValidVisibleFeeds办法内实现。
public List<CardLogFeed> getCurrentValidVisibleFeeds() { List<CardLogFeed> currentValidVisibleFeeds = new ArrayList<>(); //获取layoutManager RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); //针对线性排版 if (layoutManager instanceof LinearLayoutManager) { int firstVisibleIndex = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); int lastVisibleIndex = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); for (int i = firstVisibleIndex; i <= lastVisibleIndex; i++) { RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(i); if (viewHolder instanceof CardLogFeed && ((CardLogFeed) viewHolder).isVailLogUnit()) { currentValidVisibleFeeds.add((CardLogFeed) viewHolder); } } } //针对瀑布流排版 if (layoutManager instanceof StaggeredGridLayoutManager) { //... } //其余排版 return currentValidVisibleFeeds;}
以线性排版为例子,获取界面所有可见的 Holder 对象并判断每一个对象是否为 CardLogFeed 类型且满足无效卡片的断定条件,最终失去无效卡片集。
说完曝光,回到列表滑动时触发的 consumeCardLog 生产办法。
//1毫秒=1000000纳秒private long DURATION_STEP = 1000000;//定义最小无效生产时长 100 msprivate long MIN_CONSUME_DURATION = 100;private void consumeCardLog(long startConsumeTime) { //获取工夫距离 long consumeTime = (System.nanoTime() - startConsumeTime)/DURATION_STEP; if (consumeTime < MIN_CONSUME_DURATION) { return; } if (currentValidVisibleFeeds.isEmpty()) { return; } //卡片生产 for (CardLogFeed logFeed : currentValidVisibleFeeds) { logFeed.logConsume(consumeTime); }}
列表开始滑动时,就拿上一次曝光缓存的无效卡片做一次生产记录。
至此,实现一次残缺的曝光/生产逻辑实现,外围流程不变,外部细节可自由发挥调整。
最初做个演示,当无效卡片曝光时设置成灰色,生产之后恢复原状。
后话
信息流卡片曝光/生产数据反馈对利用内容的投放策略有很大的指导意义。这篇文章仅作为探讨整体的程序设计思路,让更多开发者在将来遇到类似的场景时可参考比拟。
如果有更好的思路或意见建议,欢送探讨。
就开发者而言,有时候接到一个需要看似简略的需要,实际上在开发过程中才遇到各种妖魔鬼怪,养成良好的设计模式对每一个程序开发者都极其重要。
记得有一次,策动让咱们做信息流的视频卡片的自动播放,说 “列表停下来后满足自动播放条件的视频卡片就播放,这应该挺简略吧?”。
我:“....”
那系列下期就聊聊信息流常见下,富媒体项的抉择思路,包含 Gif 播放/视频播放常见。
欢送关注追更。
欢送关注 「Android之禅」公众号,和你分享有价值有思考的技术文章。
可增加微信 「Ming_Lyan」备注 “进群” 退出技术交换群,探讨技术问题严禁所有广告灌水。
如有 Android 畛域有遇到技术难题亦或对将来职业规划有纳闷,一起探讨交换。
欢送来扰。