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
if (mOrientation == VERTICAL_LIST) { outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); } else { outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); }
這個outRect設置的四個值是什麼意思呢?先來看看它是在哪裏調用的,它在RecyclerView中惟一被調用的地方就是 getItemDecorInsetsForChild(View child)
函數。github
Rect getItemDecorInsetsForChild(View child) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.mInsetsDirty) { return lp.mDecorInsets; } final Rect insets = lp.mDecorInsets; insets.set(0, 0, 0, 0); final int decorCount = mItemDecorations.size(); for (int i = 0; i < decorCount; i++) { mTempRect.set(0, 0, 0, 0); mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); insets.left += mTempRect.left; insets.top += mTempRect.top; insets.right += mTempRect.right; insets.bottom += mTempRect.bottom; } lp.mInsetsDirty = false; return insets; }
能夠看到,getItemOffsets
函數中設置的值被加到了 insets
變量中,並被該函數返回,那麼 insets 又是啥呢?canvas
根據Inset Drawable文檔,它的使用場景是:當一個view須要的背景小於它的邊界時。例如按鈕圖標較小,可是咱們但願按鈕有較大的點擊熱區,一種作法是使用ImageButton,設置background="@null"
,把圖標資源設置給src
屬性,這樣ImageButton能夠大於圖標,而不會致使圖標也跟着拉伸到ImageButton那麼大。那麼使用Inset drawable也能達到這樣的目的。可是相比之下有什麼優點呢?src
屬性也能設置selector drawable,因此點擊態也不是問題。也許惟一的優點就是更「優雅」吧 :)ide
回到正題,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 中。測試
public void measureChild(View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); widthUsed += insets.left + insets.right; heightUsed += insets.top + insets.bottom; final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, canScrollHorizontally()); final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, canScrollVertically()); if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); } }
上面這段代碼中調用 getChildMeasureSpec
函數的第三個參數就是 child view 的 padding,而這個參數就把 insets 的值算進去了。那麼如今就能夠確認了,getItemOffsets 中爲 outRect 設置的4個方向的值,將被計算進全部 decoration 的尺寸中,而這個尺寸,被計入了 RecyclerView 每一個 item view 的 padding 中。ui
這一步測試主要是對 getItemOffsets 函數傳入的 outRect 參數各個值的設置,以證明上述分析的結論。
能夠看到,當 left, top, right, bottom 所有設置爲50時,RecyclerView 的每一個 item view 各個方向的 padding 都增長了,對比各類狀況,確實 getItemOffsets 中爲 outRect 設置的值都將被計入 RecyclerView 每一個 item view 的 padding 中。
先來看看官方樣例的 DividerItemDecoration
實現:
public void drawVertical(Canvas c, RecyclerView parent) { final int left = parent.getPaddingLeft(); final int right = parent.getWidth() - parent.getPaddingRight(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); final int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child)); final int bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } }
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,其實是須要的 ↩
借鑑:https://blog.piasy.com/2016/03/26/Insight-Android-RecyclerView-ItemDecoration/