標籤:
最近遇到需求,需要在某張使用者發表的圖片中展示評論,方式是以淡入淡出的彈幕形式。需求為淡入事件t1->淡出t2,所有彈幕依次開始播放,中間一定的時間間隔deltaT。仔細考慮之後,想到以下實現方式,現總結下來並進行實現、分析以及比較。
不妨認定彈幕的畫布為BarrageView;每條彈幕的繪製為BarrageItemView。
從只有一條彈幕開始思考,我們需要設計一個屬性動畫,負責淡入:
在該動畫的完成回呼函數onAnimationEnd中啟動淡齣動畫,在淡齣動畫的完成回呼函數中,對該BarrageItemView進行remove操作,這樣就完成了一條彈幕的顯示過程。
多條彈幕展示時也是同樣的邏輯。
使用這種方法,彈幕的依次播放在實現上是一個痛點:我們需要另開一個通知線程(計時器),在每隔deltaT時間,對BarrageView(彈幕畫布)進行通知,告訴它你可以播放一條新的彈幕了。
於是在每個BarrageView中我們需要建一個handle來對通知訊息進行處理,當BarrrageView是作為ListView或者RecyclerView的一個Item時,通過使用者的活動,可能會展示大量的BarrageView,這樣會構建出大量的Handler,對系統效能也是一個挑戰。不過如果處於RecyclerView的Item中,在RecyclerView對ViewHolder進行重用時,也可以在Handler實現中進行判斷和重用,以最大程度減少資源浪費。這樣的實現方法會出現一些的問題:太多的BarrageItemView和屬性動畫的建立。這樣的做法不僅會影響應用的效能,而且每條彈幕和屬性動畫的管理都會對代碼實現提出挑戰。
另一個需要注意的點是,彈幕動畫往往不是穩定的從開始播放至結束,使用者可以進行暫停、關閉、開啟彈幕等操作。這一系列管理功能決定了我們在使用屬性動畫時必須記錄每個當前現正播放的BarrageItemView的引用以及ObjectAnimator的引用。使用者在關閉彈幕時,系統需要cancel動畫,此時需要調用該動畫對應的BarrageItemView的remove操作,因為處在主線程中,而對ObjectAnimator和BarrageItemView的添加是處在Handler中的,那麼這樣的處理涉及到一個多線程訪問的問題,很有可能會造成對同一個ArrayList的添加刪除衝突,導致crash,這裡的管理實現尤其需要小心。
動畫的管理不妨考慮使用屬性動畫集合AnimatorSet,雖然在動畫的管理會方便點,但是上述的問題仍然存在,也是不可避免的。如果遇到彈幕淡入>停留->彈幕淡出邏輯,動畫數目更為之多,AnimatorSet有一定的優勢。
具體實現的過程中,我發現最大的問題是ObjectAnimator和BarrageItemView的管理,當進行彈幕的關閉(Clear)時,如果有一個彈幕的刪除出錯或者一個動畫的cancel出錯,那麼螢幕中的邏輯就會混亂。如果評論有N條,在彈幕建立時候若每次開始播放時候根據評論建立一個新的BarrageItemView,那麼如果上述邏輯出錯,BarrageView中經常會出現大量重複彈幕。所以必須對所有評論先建立所有的彈幕,收到一條通知訊息,播放一條,這樣的邏輯是比較清楚的。
核心思路是設定paint的alpha,對bitmap在ondraw方法中進行繪製,具體方案及流程如下:
1、定義一個可以被XML使用的彈幕畫布:BarrageView
2、基本變數初始化
使用一個自訂資料結構來儲存彈幕的成員變數:
在BarrageView的設定彈幕列表的函數中,對彈幕資料結構的curAlpha成員變數進行初始化,因為需要解決彈幕出現有時間間隔deltaT的問題,所以通過計算,將curAlpha值列表初始化為-*i*ALPHA_START_STEP,當curAlpha值為負數時,不對其進行繪製,在下個繪製時隙中該值會得到FADE_STEP的增加,當其大於0時開始使用paint根據畫家演算法進行覆蓋繪製。這裡ALPHA_START_STEP表示的就是各個彈幕之間的時隙deltaT。
3、建立每個彈幕的BarrageItemView,並將其轉化成Bitmap。
4、在ondraw方法中實現彈幕Bitmap的繪製邏輯:
對當前每個彈幕進行判斷,是處於淡入狀態還是淡出狀態,若是前者,將alpha值增加,用paint進行繪製,後者則為減少。
為了實現淡入->停留->淡出邏輯,可以讓alpha增加的最大值大於255,但是在真正繪製時進行判斷,若當前alpha大於255,則選用255進行繪製,即實現了停留效果。
淡入實現:
淡出實現邏輯類似,不過在淡出之後需要將地址位置重新更新,實現彈幕在下次播放時選擇另一個隨機位置。
在所有繪製進行完畢,則調用:
在下次時隙中插入介面重繪的標誌,實現了彈幕的持續展示。
值得注意的是,FADE_STEP是計算出來的alpha值變化速率,在對其進行計算時,要考慮到Android系統每隔16ms會發出VSYNC訊號通知系統進行渲染繪製。所以要注意兩點:所有在ondraw中需要執行的操作必須在16ms完成,否則會造成在下一次繪製時還未計算完,造成丟幀;不用將FADE_STEP值設定過小,因為對下一次繪製我們將手動調用觸發演算法,所以發出過多的重繪訊號也會根據系統的VSYNC訊號進行重算覆蓋,因此在此需要進行權衡。
5、實現響應事件:
因為彈幕的表現形式不再是View,而是Bitmap,所以整個彈幕畫布的點擊事件需要使用GestureDetectorCompat來控制:
這個方法經過測試發現是比較高效的,但是有一個很大的問題:如果彈幕展示只有文字,那麼該方法效率和效能都很好,也能完成需求。但是如果彈幕包涵圖片(如頭像),那麼圖片的請求將會是一個非同步過程。很難保證在Bitmap產生時圖片已經載入完畢。
該方案實現邏輯和方案二很像,不再贅述詳細邏輯,不同的是每個BarrageItemView資料結構中不再儲存Bitmap,而是直接儲存了View或者Layout。這樣可以完美地解決上述的圖片載入問題。在每次onDraw方法調用時候不再更新paint的alpha,而是擷取BarrageItemView新的alpha值,直接對其進行設定,如淡入控制:
簡單測試了下效能,使用手機機型:HUAWEI MT7-TL00;Android 4.4.2,level 19,ROM:HuaWei/EMUI/EmotionUI_3.0
隨機展示一些彈幕畫布,不均勻地含有一些彈幕,測試時包括動畫的開啟、關閉、暫停、繼續等,CPU佔用率監控結果如下:
平穩播放時:
方案一:7%-10%
方案二:3%-7%
方案三:3%-8%
為方案三的監控,分別執行了:開啟、暫停、繼續播放、關閉、開啟的操作。可見在不播放彈幕的狀態,Demo對CPU的佔用率是極低的。使用屬性動畫對CPU的佔用率最高,而且需要大量的動畫建立、刪除和執行,例如在管理時需要對屬性動畫和BarrrageItemView的引用也需要及時更新,都帶來了很大的開銷。
最後附上一個:
基於Android淡入淡出彈幕實現