前段時間打造了一款簡單易用功能全面的圖檔上傳元件,現在就來将上傳的圖檔以圖檔集的形式展現到App上。出于使用者體驗考慮,加載新圖檔采用[無限]滾動模式,Android平台上我們優選RecyclerView元件。
顯示圖檔,用的自然是<code>ImageView</code>,然而它并不支援直接加載網絡圖檔,需要先通過其它網絡元件(如<code>HttpURLConnection</code>、<code>okhttp3</code>等)将圖檔擷取到本地,得到<code>BitMap</code>資料,然後通過<code>setImageBitmap()</code>加載。
ImageView也有<code>setImageURI(Uri uri)</code>方法,這裡uri的命名容易讓人産生錯覺,其實隻能是本地檔案路徑。
所幸,一些開源元件封裝了繁瑣的網絡操作和緩存政策,提供了易用的API。這裡我選擇了<code>Glide</code>。
有兩個,一個用于清單中各個圖檔顯示,一個顯示<code>加載更多/已全部加載</code>放置在清單最末提示使用者。
RecyclerView的設計模式網上資料很多,此處不再贅述。先實作<code>RecyclerView.Adapter</code>。
為RecyclerView添加滾動監聽,在合适的時候加載新資料到資料集中。
不要将上面預加載資料和Glide的預加載圖檔混淆起來,拿到資料,和通過資料中的uri擷取圖檔并下載下傳,這是兩個步驟。Glide專門針對RecyclerView提供了預加載方案,是為了減少滑動時圖檔還未從網絡請求導緻的等待加載情況,目前隻支援<code>LinearLayoutManager</code>或其子類布局
按列瀑布流顯示圖檔。簡單地将RecyclerView的layoutManager設為StaggeredGridLayoutManager執行個體即可,注意StaggeredGridLayoutManager目前還是beta版。
使用<code>StaggeredGridLayoutManager</code>會發現上下滑動過程中,經常發生圖檔塊重排。根據網上說法,這是因為複用的<code>ViewHolder</code>和該ViewHolder要加載的圖檔,它們的尺寸不一緻導緻。比如某個ViewHolder之前加載的圖檔高度為60,之後被回收,但是尺寸資訊仍然保留着,後來被一張高度80的圖檔複用,由于StaggeredGridLayoutManager是根據ViewHolder的尺寸排序布局,尺寸的變化導緻發生多次排序。解決方法是在ViewHolder綁定資料時(在<code>RecyclerView.Adapter.onBindViewHolder()</code>中),就事先設定好本次布局的最終尺寸,如下:
當由下滑回到最頂部時,經常會出現頂部(第一行)的圖檔互相重排。仔細觀察,這是因為第一行初次布局時是按順序排列而非按空缺插入,往回滑時則是按空缺(哪裡最空最先排哪裡),這導緻順序可能與初次排序不一緻。不過還好,最終仍會按照圖檔尺寸各自歸位。而且這種情況隻會出現在第一次由下滑回到頂部時。
StaggeredGridLayoutManager一共有3k多行代碼,又是beta版。代碼潔癖的我把目光投向了GreedoLayoutManager,它是<code>500px</code>開源的一個LayoutManager,能在保持圖檔寬高比例的前提下将多張圖檔拼接到一行顯示,原理很簡單,看下面動圖:

替換LayoutManager也相當簡單,重新設定下RecyclerView的layoutManager即可。
GreedoLayoutManager在布局之前需要知道item的寬高比例,隻要讓Adapter實作<code>SizeCalculatorDelegate</code>接口即可
運作界面顯示:
可以看到每張圖檔都比預期大很多,隻能看到一小部分。經研究發現,上面定義的圖檔展示項的布局(LinearLayout内嵌ImageView),最終呈現後,LinearLayout的尺寸是每個網格的尺寸,而内嵌的ImageView則超出了LinearLayout,似乎其最終尺寸是<code>MeasuredSize</code>——我們在<code>onCreateViewHolder</code>時使用了<code>LayoutInflater.from(context).inflate(viewType, parent, false)</code>,這裡的<code>parent</code>是RecyclerView,而在布局xml中寬高都設定為<code>match_parent</code>,是以其中ImageView的MeasuredSize同RecyclerView的寬高——然而ImageView最終尺寸應該同樣适配網格尺寸才對。
以width為例:
我們看到每個框格其實是ImageView被截取的左上角那部分。
經過一番搜尋,網上各種對<code>getWidth</code>和<code>getMeasuredWidth</code>差別的闡述,并沒有解決我的困惑,直到這篇從源碼的角度分析,getWidth() 與 getMeasuredWidth() 的不同之處讓我知道,其實Android系統并沒有對width下定義,自定義布局時可随意設定子項大小,是否超出螢幕也沒有限制。在我們這個場景下,估計GreedoLayoutMananger在處理了最外層控件(這裡是LinearLayout)的width後,并沒有遞歸處理内部控件的width,進而導緻了這個bug。
既然如此,那麼就不要外圍的LinearLayout,直接使用ImageView,反倒省了一點開銷。
當然也有ViewHolder重用導緻的顯示問題,圖檔隻顯示一部分,且是按ViewHolder重用前的寬高比例顯示,如下:
懶得深究,使用Glide官方文檔建議的waitForLayout()并沒有用,<code>override(width, height)</code>提前告知圖檔尺寸解決。
使用SwipeRefreshLayout,easy,按過不表。最後成品如下
一般常用<code>detachAndScrapView</code>,RecyclerView會自動幫我們處理後續重用View[Holder]的邏輯。然而在某些場景下(如隻是重排目前顯示的Views而不是移除),我們可以使用更輕量級的<code>detachView</code>(detach之後view就不在界面上顯示了),不過要記得在下次布局之前手動調用<code>attachView</code>(位置的話,detach之前在哪,attach後就在哪)或<code>removeDetachedView</code>/<code>recycleView</code>。
注意detach之後,RecyclerView.getChildCount()就相應減少。
真正把 view layout到界面上的是RecyclerView的<code>layoutDecorated</code>方法。