聊實用功能設計系列之 信息流卡片的消費與曝光

這篇文章是《聊實用功能設計系列》的第一期,聊一聊我們線上項目信息流卡片曝光/消費的設計思路。

系列會拆分多期講解線上項目中重要功能的設計過程。側重點在于讓讀者理解功能模塊的設計意義及整體搭建思路,大部分細節實現各項目自行實現即可。

背景

市場上大部分 APP 都采用了信息流的設計形式來承載信息內容展示,不同特征產品所設計的信息流形式不盡相同。

比如圖片瀑布流,商品列表流或資訊Feed流等。

file

(淘寶商品流)

file

(懂車帝視頻流)

盡管展示方式不同,但內容在列表中的流向是一致的,也都以列表滑動的交互來曝光更多內容。

而列表內容的展示效果可通過統計列表項的曝光次數/點擊次數/曝光時刻/滑動時刻等數據進行多維度計算衡量。

如常見的點擊率消費時長

點擊率=點擊次數/曝光次數,點擊率越高則表示用戶對內容進一步了解的意向越高。

消費時長=t(滑動時刻)-t(上一次曝光時刻),消費時長越長則表示用戶對內容興趣越高。

當然,不同列表內容的計算規則有所差異,考察內容展示效果的側重點也不應相同。

如資訊類 Feed 流中內容項更多是以圖文混排的展示形式來吸引用戶進行點擊消費,更關注內容的點擊率。

而對于視頻流內短視頻快消類內容項,一般支持在流上直接播放,更關注內容的觀看消費時長。

所以信息流列表項的曝光信息/消費數據對于衡量流投放效果至關重要,那么如何獲取這些信息呢?

設計思路

以我們線上項目為例子。應用內信息流頁面非常多。

有類微博的動態卡片流。

類資訊類卡片流。

雙列視頻流。

上述僅是 3 個常見場景,如果針對所有場景列表做設計,顯然可維護性及擴展性非常差,所以必須考慮做成通用性設計。

我們約定幾個高頻詞匯含義,后續描述都采用其代替表述。

  • 卡片,任意一個信息流列表項。
  • 曝光,卡片的曝光行為。
  • 消費,卡片的消費行為。
  • 有效面積,卡片暴露在界面的可見面積。
  • 有效面積率,卡片有效面積與卡片視圖總面積的比率,一般可選 1/2 ~ 4/5
  • 有效卡片,卡片的有效面積率高于某個閾值時該卡片視為有效卡片,常見為 2/3,3/5。
  • 最小有效消費時長,當有效卡片的消費時長小于該時長時則認為用戶并無消費行為,一般可選再 100~200 毫秒。

有效面積是用于計算卡片在界面的可視大小,當某個卡片可視區域太小了,哪怕是長期停留,我們都認為用戶對其內容并無感知。

有效卡片則是用戶對卡片內容有感知能力,這些卡片有機會得到曝光及消費。

有了上述約定,我們再定義一種通用的卡片曝光/消費規則:

  1. 當列表靜止且用戶與界面無接觸時,列表內的有效卡片記一次曝光行為;
  2. 已靜止列表重新開始滑動時,原靜止列表內的有效卡片記一次消費行為。

則按照規則,卡片一次曝光消費的邏輯如下:

  1. 在列表靜止且用戶與界面無接觸時,收集可見的所有卡片;
  2. 過濾有效面積率小于 3/5 的卡片得到有效卡片集進行緩存,得到緩存集 list[當前有效卡片]
  3. 針對有效卡片集進行曝光并記錄曝光時刻點 t[exposure],實際上這個時間也是開始消費的時間 t[startConsume]
  4. 當列表開始滑動時,獲取當前時間戳減去上一次曝光時間得到該次消費時長,若消費時長不短于 100毫秒,則list[當前有效卡片] 進行消費記錄。
  5. 擴增強擴展。預留上述流程支持關鍵參數:比如是否動態調整有效面積率最小有效消費時長等。

另外,還可控制是否同一個界面內同一個卡片是否允許多次曝光,或控制卡片曝光的間隙等,按需實現即可。

具體實現

定義一個接口 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;

@Override
public 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 ms
private 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);
    }
}

列表開始滑動時,就拿上一次曝光緩存的有效卡片做一次消費記錄。

至此,完成一次完整的曝光/消費邏輯實現,核心流程不變,內部細節可自由發揮調整。

最后做個演示,當有效卡片曝光時設置成灰色,消費之后恢復原狀。

2021-01-06 10_14_49.gif

后話

信息流卡片曝光/消費數據反饋對應用內容的投放策略有很大的指導意義。這篇文章僅作為探討整體的程序設計思路,讓更多開發者在未來遇到相似的場景時可參考比較。

如果有更好的思路或意見建議,歡迎討論。

就開發者而言,有時候接到一個需求看似簡單的需求,實際上在開發過程中才遇到各種妖魔鬼怪,養成良好的設計模式對每一個程序開發者都極其重要。

記得有一次,策劃讓我們做信息流的視頻卡片的自動播放,說 “列表停下來后滿足自動播放條件的視頻卡片就播放,這應該挺簡單吧?”。

我:“....”

那系列下期就聊聊信息流常見下,富媒體項的選擇思路,包括 Gif 播放/視頻播放常見。

歡迎關注追更。

歡迎關注 「Android之禪」公眾號,和你分享有價值有思考的技術文章。
可添加微信 「Ming_Lyan」備注 “進群” 加入技術交流群,討論技術問題嚴禁一切廣告灌水。
如有 Android 領域有遇到技術難題亦或對未來職業規劃有疑惑,一起討論交流。
歡迎來擾。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容