RecyclerView 已經推出了一年多了,平常開發中也已經完全從 ListView 遷移到了 RecyclerView,但前兩天有人在一個安卓羣裏面問了個關於最頂上的 item view 加蒙層的問題,被人用 ItemDecoration 完美解決。此時我發現本身對 RecyclerView 的使用一直太過基本,更深刻更強大的功能徹底沒有涉及,像 ItemDecoration, ItemAnimator, SmoothScroller, OnItemTouchListener, LayoutManager 之類,以及 RecyclerView 重用 view 的原理。網上也有不少對 RecyclerView 使用的講解博客,要麼講的內容很是少,要麼提到了高級功能,可是並沒講代碼爲何這樣寫,每一個方法和參數的含義是什麼,像張鴻洋的博客,也講了 ItemDecoration 的使用,可是看了仍然雲裏霧裏,只能把他的代碼拿來用,並不能根據本身的需求編寫本身的 ItemDecoration。html
在這個系列中,我將對上述各個部分進行深刻研究,目標就是看了這一系列的文章以後,開發者能夠清楚快捷的根據本身的需求,編寫本身須要的各個高級模塊。本系列第一篇就聚焦在:RecyclerView.ItemDecoration。本文涉及到的完整代碼能夠在 Github 獲取。java
這個類包含三個方法 1:android
onDraw(Canvas c, RecyclerView parent, State state)
onDrawOver(Canvas c, RecyclerView parent, State state)
getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
官方樣例的 DividerItemDecoration
裏面是這樣實現的:git
1github 2canvas 3ide 4函數 5測試 |
|
這個outRect設置的四個值是什麼意思呢?先來看看它是在哪裏調用的,它在RecyclerView中惟一被調用的地方就是 getItemDecorInsetsForChild(View child)
函數。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 |
|
能夠看到,getItemOffsets
函數中設置的值被加到了 insets
變量中,並被該函數返回,那麼 insets 又是啥呢?
根據Inset Drawable文檔,它的使用場景是:當一個view須要的背景小於它的邊界時。例如按鈕圖標較小,可是咱們但願按鈕有較大的點擊熱區,一種作法是使用ImageButton,設置background="@null"
,把圖標資源設置給src
屬性,這樣ImageButton能夠大於圖標,而不會致使圖標也跟着拉伸到ImageButton那麼大。那麼使用Inset drawable也能達到這樣的目的。可是相比之下有什麼優點呢?src
屬性也能設置selector drawable,因此點擊態也不是問題。也許惟一的優點就是更「優雅」吧 :)
回到正題,getItemDecorInsetsForChild 函數中會重置 insets 的值,並從新計算,計算方式就是把全部 ItemDecoration 的 getItemOffsets 中設置的值累加起來 2,而這個 insets 其實是 RecyclerView 的 child 的 LayoutParams 中的一個屬性,它會在 getTopDecorationHeight
,getBottomDecorationHeight
等函數中被返回,那麼這個 insets 的意義就很明顯了,它記錄的是全部 ItemDecoration 所須要的 3尺寸的總和。
而在 RecyclerView 的 measureChild(View child, int widthUsed, int heightUsed)
函數中,調用了 getItemDecorInsetsForChild,並把它算在了 child view 的 padding 中。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 |
|
上面這段代碼中調用 getChildMeasureSpec
函數的第三個參數就是 child view 的 padding,而這個參數就把 insets 的值算進去了。那麼如今就能夠確認了,getItemOffsets 中爲 outRect 設置的4個方向的值,將被計算進全部 decoration 的尺寸中,而這個尺寸,被計入了 RecyclerView 每一個 item view 的 padding 中。
這一步測試主要是對 getItemOffsets 函數傳入的 outRect 參數各個值的設置,以證明上述分析的結論。
能夠看到,當 left, top, right, bottom 所有設置爲50時,RecyclerView 的每一個 item view 各個方向的 padding 都增長了,對比各類狀況,確實 getItemOffsets 中爲 outRect 設置的值都將被計入 RecyclerView 每一個 item view 的 padding 中。
先來看看官方樣例的 DividerItemDecoration
實現:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 |
|
drawVertical 是爲縱向的 RecyclerView 繪製 divider,遍歷每一個 child view 4 ,把 divider 繪製到 canvas 上,而 mDivider.setBounds
則設置了 divider 的繪製範圍。其中,left 設置爲 parent.getPaddingLeft(),也就是左邊是 parent 也就是 RecyclerView 的左邊界加上 paddingLeft 以後的位置,而 right 則設置爲了 RecyclerView 的右邊界減去 paddingRight 以後的位置,那這裏左右邊界就是 RecyclerView 的內容區域 5了。top 設置爲了 child 的 bottom 加上 marginBottom 再加上 translationY,這其實就是 child view 的下邊界 6,bottom 就是 divider 繪製的下邊界了,它就是簡單地 top 加上 divider 的高度。
這一步測試主要是對 onDraw 函數中對 divider 的繪製邊界的設置。
能夠看到,當咱們把 left, right, top 7 設置得和官方樣例同樣,bottom 設置爲 top + 25
,注意,這裏 getItemOffsets 對 outSets 的設置只有 bottom = 50
,也就是 decoration 高度爲50,咱們能夠看到,decoration 的上半部分就繪製爲黑色了,下半部分沒有繪製。而若是設置top = child.getBottom() + params.bottomMargin - 25
,bottom = top + 50
,就會發現 child view 的底部出現了 overdraw。因此這裏咱們能夠得出結論:在 onDraw 爲 divider 設置繪製範圍,並繪製到 canvas 上,而這個繪製範圍能夠超出在 getItemOffsets 中設置的範圍,但因爲 decoration 是繪製在 child view 的底下,因此並不可見,可是會存在 overdraw。
有一點須要注意:decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver,這三者是依次發生的。而因爲 onDrawOver 是繪製在最上層的,因此它的繪製位置並不受限制(固然,decoration 的 onDraw 繪製範圍也不受限制,只不過不可見),因此利用 onDrawOver 能夠作不少事情,例如爲 RecyclerView 總體頂部繪製一個蒙層,或者爲特定的 item view 繪製蒙層。這裏就不單獨進行測試了,請見下一節的總體效果。
實現的效果:除了最後一個 item view,底部都有一個高度爲25的黑色 divider,爲整個 RecyclerView 的頂部繪製了一個漸變的蒙層。效果圖以下:
不算被 Deprecated 的方法
把 left, top, right, bottom 4個屬性分別累加
也就是在 getItemOffsets 函數中爲 outRect 參數設置的4個屬性值
child view,並非 adapter 的每個 item,只有可見的 item 纔會繪製,纔是 RecyclerView 的 child view
能夠類比 CSS 的盒子模型,一個 view 包括 content, padding, margin 三個部分,content 和 padding 加起來就是 view 的尺寸,而 margin 不會增長 view 的尺寸,可是會影響和其餘 view 的位置間距,可是安卓的 view 沒有 margin 的合併
bottom 就是 content 的下邊界加上 paddingBottom,而爲了避免「吃掉」 child view 的底部邊距,因此就加上 marginBottom,而 view 還能設置 translation 屬性,用於 layout 完成以後的再次偏移,同理,爲了避免「吃掉」這個偏移,因此也要加上 translationY
這裏因爲並無對 child view 設置 translation,爲了代碼簡短,就沒有減去 translationY,其實是須要的