用樹型模型管理App數字和紅點提示(附Demo)


我們平常接觸到的大部分App,在收到新消息的時候一般都會以數字或紅點的形式提示出來。比如在微信當中,當某位好友給我們發來新的聊天消息的時候,在相應的會話上就會有一個數字來表示未讀消息的數目;再比如當微信朋友圈裏有人發佈新的內容時,朋友圈的入口就會出現一個紅點,而當朋友圈裏有人給我們點了贊,或者對我們發佈的內容進行了評論的時候,朋友圈的入口就會顯示一個數字。


但是,我們在試用一些新的App產品時,總會發現它們在數字和紅點展示上存在各種各樣的問題。比如,紅點怎麼點擊也清除不掉;或者,發現有數字了,點進去卻什麼也沒有;或者,點進去看到的數字和外面看到的不一樣。


那這些問題到底是怎樣產生的呢?


我猜測,問題產生的根源是:沒有對數字和紅點的展示邏輯做一個統一的抽象和管理,以至於各種數字和紅點之間的關係錯綜複雜,牽一髮而動全身。這樣,在App的維護過程中,稍微有一點改動(比如增加幾個數字或紅點類型),出現問題的概率就很高。


本文會提出一個樹型結構模型,來對數字和紅點的層次結構進行統一管理,並會在文章最後給出一個可以運行的Android版的Demo程序,以供參考。


如果您現在手頭正好有一部Android手機,那麼您可以先掃描識別下面的二維碼下載安裝這個Demo,花幾分鐘看看它是否對您有用。注意:在微信中的正確操作步驟是,識別二維碼後,點擊右上角的“在瀏覽器中打開”,然後才能跳轉到下載頁面,最終完成下載(請在百度雲的下載頁面點擊“普通下載”)。


圖片


樸素的數字紅點管理方式


爲了討論方便,我們首先對一般情況下數字和紅點展示的需求做一個簡單的整理,然後看看根據這樣的需求最直觀的實現方式可能是怎樣的。


  • 有些新消息是重要的,需要展示成數字;有些新消息不那麼重要,需要展示成紅點。比如,我收到了新評論,或收到了新的點贊,以數字表示比較合理;而對於一些系統發給我的系統消息,我希望它不會太乾擾到我的視線,這時以比較輕的紅點形式展示比較合理。

  • 數字和紅點是需要分級展示的。當有新消息到來時,用戶可以從App首頁(即第一級頁面)出發,根據數字和紅點提示,逐級深入到更深的頁面,最終到達展示新消息的終端頁面。比如在下面的App截圖中,當用戶收到新評論的時候,首先會在第2個Tab(即“消息”那個Tab)上出現數字提示,引導用戶進入第2個Tab頁面,然後在頁面中“收到的評論”旁邊會繼續顯示數字提示,引導用戶點擊進入更深一級的評論頁面。


圖片


  • 如果某一級的數字提示,在它更深一級的頁面上包含多個數字提示,那麼本級數字應該是更深一級頁面的數字之和。比如上圖中的消息數5=4+1。

  • 如果某一級的數字(紅點)提示,在它更深一級的頁面上既有數字也有紅點,那麼本級優先按數字展示;如果更深一級的頁面上數字都被清掉了,只有紅點了,那麼本級才按照紅點展示。比如下面的App截圖中,頁面上只有系統消息了,而系統消息是展示紅點,所以第2個Tab上也變成紅點展示了。


圖片


相信以上總結的幾點,跟大多數App的展示邏輯大體類似。即使有一些差別,應該也不妨礙我們接下來的討論。


好,現在我們就以上面App截圖中的具體情形來考慮一下實現。“消息”Tab包含“收到的評論”、“收到的贊”和“系統消息”,其中評論和贊是數字,系統消息是紅點。


我們單獨考慮“消息”這個Tab上的數字紅點展示邏輯,不難寫出類似如下的代碼(僞碼):


圖片


這段代碼當然能實現需求,但是缺點也是很明顯的。其中最關鍵的是,它要求在“消息”這個Tab上的展示邏輯要列舉下面包含的所有子消息類型(評論、贊、系統消息),並且知道每個類型是數字還是紅點。上面只是給出了兩級頁面的情況,如果出現三級頁面甚至更多級呢?那麼這些信息就要在各級頁面上重複一遍。


這會造成維護和修改變得複雜。想象一下,在“消息”下面又增加了一個新的消息類型,或者某個類型的消息從數字展示變成紅點展示了,甚至是某個類型的消息,從一個頁面棧移動到了另一個頁面棧了。所有這些情況,都要求更高層級的所有頁面都對應進行修改。當一個App的消息類型越來越多,達到幾十個的時候,可以想象這種修改是很容易出錯的。


基於樹型模型的數字紅點管理方式


上面說的問題,我們在微愛App開發的初期也遇到過。後來,我們重新審視了App中紅點和數字展示的結構,使用樹型結構來看待它,讓維護工作變得簡單。


一個App的頁面本身就是分級的,對於頁面的訪問路徑本質上就是個樹型結構。


圖片

如上圖所示,節點1代表第1級頁面,這個頁面下面包含三個更深一級(第2級)的頁面入口,分別對應節點2,3,4。再深一級就到了終端頁面,以綠色的方形節點表示。


這個樹型的模型可以如下表述:


  • 葉子節點(綠色方形的節點)表示最終要展示消息的終端頁面。消息在葉子節點上如何展示,是產品設計的時候就定好的。比如,它可以直接把消息展示出來,或者先展示一個數字,點進去再展示消息內容(就像前面App截圖中的評論數提示),也或者可以彈框來提示。總之,它的展示樣式是固化在產品業務的代碼中的。

  • 中間節點(圓形的橙色節點)表示從第1級頁面到達消息終端頁面訪問路徑上的頁面。中間節點上的展示一般就是數字或紅點。

  • 每一個消息類型,我們稱爲一個Badge Number。它具有三個屬性:

    • type: Badge Number類型。

    • count: 計數,對於每個Badge Number,每個用戶一個計數。

    • displayMode: 當前badge number在父節點上的顯示方式。0表示紅點,1表示數字。

  • Badge Number根據所屬業務類型的不同,分屬不同的大類(Category)。每個大類內的Badge Number類型type分配在同一個類型區間內。比如上面樹型結構圖中2,3,4節點就分別對應三個業務類型,也就是三個大類,它們對應的類型區間分別爲[A, C], [X, Y], [R, T]。再舉一個實際的例子,比如微信朋友圈是一個業務大類,裏面的Badge Number類型包括:有人評論我(數字),有人給我點贊(數字),好友有新消息發佈(紅點),等。


爲了使得一個大類內的Badge Number能用一個類型區間來表達,我們在爲類型分配值的時候,可以採取類似這樣的方式:用一個int來表示Badge Number類型,而它的高16位用來表示大類。比如“消息”大類高16位是0x2的話,那麼它包含的三種Badge Number類型(type)就可以這樣分配:


  • 收到的評論:(0x2 « 16) + 0x1

  • 收到的贊:(0x2 « 16) + 0x2

  • 系統消息:(0x2 « 16) + 0x3


這樣,“消息”這一大類就可以用一個類型區間[(0x2 « 16) + 0x1, (0x2 « 16) + 0x3]來表達。


有了類型區間之後,我們重新看一下樹型模型裏面的中間節點。它們都可以用一個或多個類型區間來表示。它們的展示邏輯(是展示成數字,還是紅點,還是隱藏),需要對所有子樹的類型區間求和。具體求和過程是:


  • 先對所有類型區間裏的數字類型進行求和,如果大於0,則展示數字;否則,

  • 對所有類型區間裏的紅點類型進行求和,如果大於0,則展示紅點;否則,

  • 隱藏數字和紅點。


樹型模型的代碼實現


樹型模型的實現,我們稱爲Badge Number Tree,本文提供了一個Android版的Demo實現,源碼可以從GitHub下載:https://github.com/tielei/BadgeNumberTree 。


下面我們把關鍵部分分析一下。


Android版本的主要實現類爲BadgeNumberTreeManager,它的關鍵代碼如下(爲了不影響我們理解主要邏輯,非關鍵代碼在下面忽略了,沒有貼出。如需查看請到GitHub下載源代碼;下面代碼請點擊看大圖):


圖片


在這段代碼中我們需要注意的點包括:


  • 前面對於Badge Number的增刪改查4個操作——setBadgeNumber、addBadgeNumber、clearBadgeNumber、getBadgeNumber,它們都比較簡單,實現代碼這裏沒有貼出來。實際上在Demo中,是基於SQLite本地存儲來實現的。我們需要注意的是各個操作的應用場景:

    • setBadgeNumber用於一般的新消息提醒,在新消息提醒產生時被調用,將Badge Number存入本地。這些Badge Number中的count值由服務器來維護,所以以服務器爲準,每次從服務器獲取到之後,就調動setBadgeNumber覆蓋本地的值。

    • addBadgeNumber用於本地累加計數的消息提醒,比如聊天消息。一個用戶接收的新聊天消息是依靠本地計數的,因此使用addBadgeNumber累加計數。

    • clearBadgeNumber用於清除指定類型的Badge Number。通常來說,當用戶在消息終端頁面(樹型的葉子節點)上閱讀完新消息後,需要清除Badge Number。

    • getBadgeNumber,根據指定類型獲取Badge Number的值,用於在消息終端頁面(樹型的葉子節點)上展示消息的時候調用。

  • 最後有一個private的getBadgeNumber方法,它和前面public的重載方法不同,它不是取指定的某一個類型的Badge Number,而是取一個類型區間[typeMin, typeMax]裏的指定顯示方式(displayMode)的Badge Number總數。這個方法是實現中間節點上Badge Number展示邏輯的基礎。這裏的實現代碼也沒有貼出來,它的實現其實也比較簡單,在Demo中是基於SQLite做的一個求和(sum)操作來實現的。

  • public的getTotalBadgeNumberOnParent是一個關鍵的方法,它用於實現中間節點上Badge Number展示邏輯。輸入的typeIntervalList參數是一個類型區間的列表,對應一箇中間節點。它的異步輸出參數是一個BadgeNumberCountResult對象,可以表達三種展示結果:數字、紅點、隱藏(無顯示)。這個方法的實現是調用了它的另一個私有重載方法,先後對類型區間列表上的數字類型和紅點類型分別進行求和(這就是前面講的對中間節點所有子樹類型區間求和的實現)。


調用getTotalBadgeNumberOnParent的代碼例子如下:


圖片


關於實現上的一些補充說明


  • 在Demo程序中,BadgeNumberTreeManager的底層存儲使用的是SQLite。但是,由於BadgeNumberTreeManager的接口調用很頻繁,因此在實現中還加入了中間一級內存緩存(詳見GitHub代碼)。

  • 客戶端通過某種方式獲取到新的Badge Number後,將它存入本地(通過BadgeNumberTreeManager的setBadgeNumber和addBadgeNumber接口)。而客戶端獲取Badge Number的方式可能有多種,比如通過長連接推送到客戶端(App自己實現的長連接,或者第三方平臺的長連接),或者通過HTTP服務拉取得到(這種方式適用於實時性不強的新提示)。

  • 中間節點Badge Number的展示刷新邏輯(即調用BadgeNumberTreeManager的getTotalBadgeNumberOnParent接口),需要在必需的所有時機執行。以本文給出的Android版Demo爲例,這些時機包括:頁面onResume的時候,子Tab切換的時候,獲取到新的Badge Number的時候。展示刷新邏輯執行的時機不精確,或者有遺漏,也是App數字紅點展示出現問題的一個常見原因。

  • 中間節點Badge Number的清除,常見的有兩種情況:(1)所有子節點都清除了它才清除;(2)只要點擊了就清除,而不管子節點是否都清除了。本文給出的Demo是按前一種情況實現的。如果想實現後一種情況,需要爲每個中間節點再單獨記錄一個標記,但這個改動並不大。

  • 雖然本文給出的代碼示例是基於Android Java的,但本文給出的樹型模型,也可以用於非Android Java版本的App實現。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章