一个优秀的可定制化Flutter相册组件,看这一篇就够了
背景
在做圖片、視頻相關(guān)功能的時候,相冊是一個繞不開的話題,因為大家基本都有從相冊獲取圖片或者視頻的需求。最直接的方式是調(diào)用系統(tǒng)相冊接口,基本功能是滿足的,一些高級功能就不行了,例如自定義UI、多選圖片等。
我們調(diào)研了官方的image_picker,它也是調(diào)用系統(tǒng)的相冊接口來處理的,可定制程度不高,不能滿足我們的要求。所以我們選擇自己來開發(fā)Flutter相冊組件。
我們的組件需要有如下的功能:
- 在app內(nèi)完成圖片、視頻的選取,完全不用依賴系統(tǒng)相冊組件
- 可以多選圖片,支持指定選定圖片的總數(shù)目
- 在多選的時候UI反應(yīng)出選擇的序號。
- 可以控制視頻、圖片的選擇。例如:只讓用戶選擇視頻,圖片是灰色的。
- 大圖預(yù)覽的時候可以放大縮小,也可直接加入到選取列表。
設(shè)計思路
API使用簡單,功能豐富靈活,具有較高的訂制性。業(yè)務(wù)方可以選擇完全接入組件,也可以選擇在組件上面進行UI定制。
Flutter做UI展現(xiàn)層,具體的數(shù)據(jù)由各Native平臺提供。這種模式,天然從工程上把UI代碼和數(shù)據(jù)代碼進行了隔離。我們在開發(fā)一個native組件的時候常常會使用MVC架構(gòu)。Flutter組件的開發(fā)的思路也基本類似。整體架構(gòu)如下:
可以看出,在Flutter側(cè)是一個典型的MVC架構(gòu),這里Widget就是View,View和Model綁定,在Model改變的時候View會重新build反映出Model的變化。View的事件會觸發(fā)Controller去Native獲取數(shù)據(jù)然后更新Model。Native和Flutter通過Method Channel進行通信,兩層之間沒有強依賴關(guān)系,只需要按約定的協(xié)議進行通信即可。
Native側(cè)的組成部分,UIAdapter主要是負責(zé)機型的適配、劉海屏、全面屏之類的識別。Permission負責(zé)媒體讀寫權(quán)限的申請?zhí)幚怼ache主要負責(zé)緩存GPU紋理,在大圖預(yù)覽的時候提高響應(yīng)速度。Decoder負責(zé)解析Bitmap,OpenGL負責(zé)Bitmap轉(zhuǎn)紋理。
需要說明的是:我們的這一套實現(xiàn)依賴于flutter外接紋理。在整個相冊組件看到的大多數(shù)圖片都是一個GPU紋理,這樣給java堆內(nèi)存的占用相對于以前的相冊實現(xiàn)有大幅的降低。在低端機上面如果使用原生的系統(tǒng)相冊,由于內(nèi)存的原因,app有被系統(tǒng)殺掉的風(fēng)險。現(xiàn)象就是,從系統(tǒng)相冊返回,app重新啟動了。使用Flutter相冊組件,在低端機上面體驗會有所改觀。
一些細節(jié)
1分頁加載
相冊列表需要加載大量圖片,Flutter的GridView組件有好幾個構(gòu)造函數(shù),比較容易犯的錯誤是使用了第一個函數(shù),這需要在一開始就提供大量的widget。應(yīng)該選擇第二個構(gòu)造函數(shù),GridView在滑動的時候會回調(diào)IndexedWidgetBuilder來獲取widget,相當(dāng)于一種懶加載。
GridView.builder({...List<Widget> children = const <Widget>[],...}) GridView.builder({...@required IndexedWidgetBuilder itemBuilder,int itemCount,...})滑動過程中,圖片滑過后,也就是不可見的時候要進行資源的回收,我們這里這里對應(yīng)的就是紋理的刪除。不斷的滑動GridView,內(nèi)存在上升后會處于穩(wěn)定,不會一直增長。如果快速的來回滑動紋理會反復(fù)的創(chuàng)建和刪除,這樣會有內(nèi)存的抖動,體驗不是很好。
于是,我們維護了一個圖片的狀態(tài)機,狀態(tài)有None,Loading,Loaded,Wait_Dispose,Disposed。開始加載的時候,狀態(tài)從None進入Loading,這個時候用戶看到的是空白或者是占位圖,當(dāng)數(shù)據(jù)回調(diào)回來會把狀態(tài)設(shè)置為Loaded的這時候會重新build widget樹來顯示圖片icon,當(dāng)用戶滑走的時候狀態(tài)進入 Wait_Dispose,這時候并不會馬上Dispose,如果用戶又滑回來則會從Wait_Dispose進入Loaded狀態(tài),不會繼續(xù)Dispose。如果用戶沒有往回滑則會從Wait_Dispose進入Disposed狀態(tài)。當(dāng)進入Disposed狀態(tài)后,再需要顯示該圖片的時候就需要重新走加載流程了。
2 相冊大圖展示:
當(dāng)點擊GridView的某張圖片的時候會進行這張圖片的大圖展示,方便用戶查看的更清楚。我們知道相機拍攝的圖片分辨率都是很高的,如果完全加載,內(nèi)存會有很大的開銷,所以我們在Decode Bitmap的時候進行了縮放,最高只到1080p。大圖展示可以概括為三個步驟。
- 1 從文件Decode出Bitmap
- 2 Bitmap轉(zhuǎn)換成為紋理,并釋放Bitmap
- 3 紋理交給Flutter進行展示
在步驟1中,Android原生的Bitmap Decode經(jīng)驗同樣適用,先Decode出Bitmap的寬高,然后根據(jù)要展示的大小計算出縮放倍數(shù), 然后Decode出需要的Bitmap。
Android相冊的圖片大多是有旋轉(zhuǎn)角度的,如果不處理直接顯示,會出現(xiàn)照片旋轉(zhuǎn)90度的問題,所以需要對Bitmap進行旋轉(zhuǎn),采用Matrix旋轉(zhuǎn)一張1080p的圖片在我的測試機器上面大概需要200ms,如果使用OpenGL的紋理坐標(biāo)進行旋轉(zhuǎn),大于只需要10ms左右,所以采用OpenGl進行紋理的旋轉(zhuǎn)是一個較好的選擇。
在進行大圖預(yù)覽的時候會進入一個水平滑動的PageView,Flutter的PageView一般來說是不會去主動加載相鄰的page的。舉個例子,在顯示index是5的page的時候index為4,6的page也不會提前創(chuàng)建的。這里有一個取巧的辦法,對于PageController的viewportFraction參數(shù)我們可以設(shè)置成為0.9999。對于前面這個例子,就是在顯示index是5的page的時候,index為4,6的page也需要顯示0.0001。這樣index為4,6的page顯示不到1個像素,基本上看不出來:
PageController(viewportFraction=0.9999)還有另外一種辦法,就是在Native側(cè)做預(yù)加載。例如:在加載第5張圖片的時候,相鄰的4,6的圖片紋理提前進行加載,當(dāng)滑動到4,6的時候直接使用緩存的紋理。
紋理緩存后,一個直接的問題:什么時候釋放紋理?等到預(yù)覽頁面退出的時候釋放所有的紋理顯示不是很合適,如果用戶一直瀏覽內(nèi)存則會無限增長。所以,我們維護了一個5個紋理的LRU緩存,在滑動過程中,最老的紋理會被釋放掉。在頁面退出的時候整個LRU的緩存會進行銷毀。
3 關(guān)于內(nèi)存
相冊圖片使用GPU紋理,會大幅減少Java堆內(nèi)存的占用,對整個app的性能有一定的提升。需要注意的是,GPU的內(nèi)存是有限的需要在使用完畢后及時刪除,不然會有內(nèi)存的泄漏的風(fēng)險。另外,在Android平臺刪除紋理的時候需要保證在GPU線程進行,不然刪除是沒有效果的。
在華為P8,Android5.0上面進行了對比測試,Flutter相冊和原native相冊總內(nèi)存占用基本一致,在GridView列表頁面,新增最大內(nèi)存13M左右。它們的區(qū)別在于原native相冊使用的是Java堆內(nèi)存,Flutter相冊使用的是Native內(nèi)存。
總結(jié)
相冊組件API簡單、易用,高度可定制。Flutter側(cè)層次分明,有UI訂制需求的可以重寫Widget來達到目的。另外這是一個不依賴于系統(tǒng)相冊的相冊組件,自身是完備的,能夠和現(xiàn)有的app保持UI、交互的一致性。同時為后面支持更多和相冊相關(guān)的玩法打好基礎(chǔ)。
后續(xù)計劃
由于我們使用的是GPU紋理,可以考慮支持顯示高清4K圖片,而且客戶端內(nèi)存不會有太大的壓力。但是4k圖片的Bitmap轉(zhuǎn)紋理需消耗更多的時間,UI交互上面需要做些loading狀態(tài)的支持。
組件功能豐富,穩(wěn)定后,進行開源,回饋給社區(qū)。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的一个优秀的可定制化Flutter相册组件,看这一篇就够了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 阿里云环境中TLS/SSL握手失败的场景
- 下一篇: 趣头条基于 Flink 的实时平台建设实