Android L面世以後,Google就推薦在開發項目中使用RecyclerView來取代ListView,由於RecyclerView的靈活性跟性能都要比ListView更強,可是,帶來的問題也很多,好比:列表分割線都要開發者本身控制,再者,RecyclerView的測量與佈局的邏輯都委託給了本身LayoutManager來處理,若是須要對RecyclerView進行改造,相應的也要對其LayoutManager進行定製。本文主要就以如下場景給出RecyclerView使用參考:git
如何實現帶分割線的列表式RecyclerViewgithub
如何實現帶分割線網格式RecyclerViewapp
如何實現全展開的列表式RecyclerView(好比:嵌套到ScrollView中使用)ide
如何實現全展開的網格式RecyclerView(好比:嵌套到ScrollView中使用)函數
先看一下實現樣式,爲了方便控制,邊界的均不設置分割線,方便定製,若是須要能夠採用Padding或者Margin來實現。Github鏈接 RecyclerItemDecoration佈局
首先看一下最簡單的縱向線性RecyclerView,通常用如下代碼:性能
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); mRecyclerView.setLayoutManager(linearLayoutManager);
以上就是最簡單的線性RecyclerView的實現,但默認不帶分割線,若是想要使用好比20dp的黑色做爲分割線,就須要本身定製,Google爲RecyclerView提供了ItemDecoration,它的做用就是爲Item添加一些附屬信息,好比:分割線,浮層等。this
RecyclerView提供了addItemDecoration接口與ItemDecoration類用來定製分割線樣式,那麼,在RecyclerView源碼中,是怎麼用使用ItemDecoration的呢。與普通View的繪製流程一致,RecyclerView也要通過measure->layout->draw,而且在measure、layout以後,就應該按照ItemDecoration的限制,爲RecyclerView的分割線挪出空間。RecyclerView的measure跟Layout其實都是委託給本身的LayoutManager的,在LinearLayoutManager測量或者佈局時都會直接或者間接調用RecyclerView的measureChildWithMargins函數,而measureChildWithMargins函數會進一步找到addItemDecoration添加的ItemDecoration,經過其getItemOffsets函數獲取所需空間信息,源碼以下:spa
public void measureChildWithMargins(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() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, canScrollHorizontally()); final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, canScrollVertically()); if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { child.measure(widthSpec, heightSpec); } }
可見measureChildWithMargins會首先經過getItemDecorInsetsForChild計算出每一個child的ItemDecoration所限制的邊界信息,以後將邊界所需的空間做爲已用空間爲child構造MeasureSpec,最後用MeasureSpec對child進行尺寸測量:child.measure(widthSpec, heightSpec);來看一下getItemDecorInsetsForChild函數:code
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; }
通常而言,不會同時設置多類ItemDecoration,太麻煩,對於普通的線性佈局列表,其實就簡單設定一個自定義ItemDecoration便可,其中outRect參數主要是控制每一個Item上下左右的分割線所佔據的寬度跟高度,這個尺寸跟繪製的時候的尺寸應該對應(若是須要繪製的話),看一下LinearItemDecoration的getItemOffsets實現:
@Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { if (mOrientation == VERTICAL_LIST) { <!--垂直方向 ,最後一個不設置padding--> if (parent.getChildAdapterPosition(view) < parent.getAdapter().getItemCount()1) { outRect.set(0, 0, 0, mSpanSpace); } else { outRect.set(0, 0, 0, 0); } } else { <!--水平方向 ,最後一個不設置padding--> if (parent.getChildAdapterPosition(view) < parent.getAdapter().getItemCount()1) { outRect.set(0, 0, mSpanSpace, 0); } else { outRect.set(0, 0, 0, 0); } } }
measure跟layout以後,再來看一下RecyclerView的onDraw函數, RecyclerView在onDraw函數中會調用ItemDecoration的onDraw,繪製分割線或者其餘輔助信息,ItemDecoration 支持上下左右四個方向定製佔位分割線等信息,具體要繪製的樣式跟位置都徹底由開發者肯定,因此自由度很是大,其實若是不是太特殊的需求的話,onDraw函數徹底能夠不作任何處理,僅僅用背景色就能夠達到簡單的分割線的目的,固然,若是想要定製一些特殊的圖案之類的需話,就須要本身繪製,來看一下LinearItemDecoration的onDraw(只看Vertical的)
@Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { if (mOrientation == VERTICAL_LIST) { drawVertical(c, parent); } else { ... } }
其實,若是不是特殊的繪製需求,好比顯示七彩的,或者圖片,徹底不須要任何繪製,若是必定要繪製,注意繪製的尺寸區域跟原來getItemOffsets所限制的區域一致,繪製的區域過大不只不會顯示出來,還會引發過分繪製的問題:
public void drawVertical(Canvas c, RecyclerView parent) { int totalCount = parent.getAdapter().getItemCount(); 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 + mVerticalSpan; final int left = child.getLeft() + params.leftMargin; final int right = child.getRight() + params.rightMargin; if (!isLastRaw(parent, i, mSpanCount, totalCount)) if (childCounti > mSpanCount) { drawable.setBounds(left, top, right, bottom); drawable.draw(c); } } }
網格式RecyclerView的處理流程跟上面的線性列表相似,不過網格式的須要根據每一個Item的位置爲其設置好邊距,好比最左面的不須要左邊佔位,最右面的不須要右面的佔位,最後一行不須要底部的佔位,以下圖所示
RecyclerView的每一個childView都會經過getItemOffsets來設置本身ItemDecoration,對於網格式的RecyclerView,須要在四個方向上對其ItemDecoration進行限制,來看一下其實現類GridLayoutItemDecoration的getItemOffsets:
@Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { final int position = parent.getChildAdapterPosition(view); final int totalCount = parent.getAdapter().getItemCount(); int left = (position % mSpanCount == 0) ? 0 : mHorizonSpan; int bottom = ((position + 1) % mSpanCount == 0) ? 0 : mVerticalSpan; if (isVertical(parent)) { if (!isLastRaw(parent, position, mSpanCount, totalCount)) { outRect.set(left, 0, 0, mVerticalSpan); } else { outRect.set(left, 0, 0, 0); } } else { if (!isLastColumn(parent, position, mSpanCount, totalCount)) { outRect.set(0, 0, mHorizonSpan, bottom); } else { outRect.set(0, 0, 0, bottom); } } }
其實上面的代碼就是根據RecyclerView滑動方向(橫向或者縱向)以及child的位置(是否是最後一行或者最後一列),對附屬區域進行限制,一樣,若是不是特殊的分割線樣式,經過背景就基本能夠實現需求,不用特殊draw。
RecyclerView全展開的邏輯跟分割線不一樣,全展開主要是跟measure邏輯相關,簡單看一下RecyclerView(v-22版本,相對簡單)的measure源碼:
@Override protected void onMeasure(int widthSpec, int heightSpec) { ... <!--關鍵代碼,若是mLayout(LayoutManager)非空,就採用LayoutManager的mLayout.onMeasure--> if (mLayout == null) { defaultOnMeasure(widthSpec, heightSpec); } else { mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); } mState.mInPreLayout = false; // clear }
由以上代碼能夠看出,在爲RecyclerView設置了LayoutManager以後,RecyclerView的measure邏輯其實就是委託給了它的LayoutManager,這裏以LinearLayoutManager爲例,不過LinearLayoutManager源碼裏面並無重寫onMeasure函數,也就是說,對於RecyclerView的線性樣式,對於尺寸的處理採用的是跟ViewGroup同樣的處理,徹底由父控件限制,不過對於v-23裏面有了一些修改,就是增長了對wrap_content的支持。既然這樣,咱們就能夠把設置尺寸的時機放到LayoutManager的onMeasure中,對全展開的RecyclerView來講,其實就是將全部child測量一遍,以後將每一個child須要高度或者寬度累加,看一下ExpandedLinearLayoutManager的實現:在測量child的時候,採用RecyclerView的measureChildWithMargins,該函數已經將ItemDecoration的佔位考慮進去,以後經過getDecoratedMeasuredWidth獲取真正須要佔用的尺寸。
@Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { final int widthMode = View.MeasureSpec.getMode(widthSpec); final int heightMode = View.MeasureSpec.getMode(heightSpec); final int widthSize = View.MeasureSpec.getSize(widthSpec); final int heightSize = View.MeasureSpec.getSize(heightSpec); int measureWidth = 0; int measureHeight = 0; int count; if (mMaxItemCount < 0 || getItemCount() < mMaxItemCount) { count = getItemCount(); } else { count = mMaxItemCount; } for (int i = 0; i < count; i++) { int[] measuredDimension = getChildDimension(recycler, i); if (measuredDimension == null || measuredDimension.length != 2) return; if (getOrientation() == HORIZONTAL) { measureWidth = measureWidth + measuredDimension[0]; <!--獲取最大高度--> measureHeight = Math.max(measureHeight, measuredDimension[1]); } else { measureHeight = measureHeight + measuredDimension[1]; <!--獲取最大寬度--> measureWidth = Math.max(measureWidth, measuredDimension[0]); } } measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight; measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth; if (getOrientation() == VERTICAL && measureWidth > widthSize) { measureWidth = widthSize; } else if (getOrientation() == HORIZONTAL && measureHeight > heightSize) { measureHeight = heightSize; } setMeasuredDimension(measureWidth, measureHeight); }
private int[] getChildDimension(RecyclerView.Recycler recycler, int position) { try { int[] measuredDimension = new int[2]; View view = recycler.getViewForPosition(position); //測量childView,以便得到寬高(包括ItemDecoration的限制) super.measureChildWithMargins(view, 0, 0); //獲取childView,以便得到寬高(包括ItemDecoration的限制),以及邊距 RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams(); measuredDimension[0] = getDecoratedMeasuredWidth(view) + p.leftMargin + p.rightMargin; measuredDimension[1] = getDecoratedMeasuredHeight(view) + p.bottomMargin + p.topMargin; return measuredDimension; } catch (Exception e) { Log.d("LayoutManager", e.toString()); } return null; }
全展開的網格式RecyclerView的實現跟線性的十分類似,惟一不一樣的就是在肯定尺寸的時候,不是將每一個child的尺寸疊加,而是要將每一行或者每一列的尺寸疊加,這裏假定行高或者列寬都是相同的,其實在使用中這兩種場景也是最多見的,看以下代碼,其實除了加了行與列判斷邏輯,其餘基本跟上面的全展開線性的相似。
@Override public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) { final int widthMode = View.MeasureSpec.getMode(widthSpec); final int heightMode = View.MeasureSpec.getMode(heightSpec); final int widthSize = View.MeasureSpec.getSize(widthSpec); final int heightSize = View.MeasureSpec.getSize(heightSpec); int measureWidth = 0; int measureHeight = 0; int count = getItemCount(); int span = getSpanCount(); for (int i = 0; i < count; i++) { measuredDimension = getChildDimension(recycler, i); if (getOrientation() == HORIZONTAL) { if (i % span == 0 ) { measureWidth = measureWidth + measuredDimension[0]; } measureHeight = Math.max(measureHeight, measuredDimension[1]); } else { if (i % span == 0) { measureHeight = measureHeight + measuredDimension[1]; } measureWidth = Math.max(measureWidth, measuredDimension[0]); } } measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight; measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth; setMeasuredDimension(measureWidth, measureHeight); }
最後附上橫向滑動效果圖:
以上就是比較通用的RecyclerView使用場景及所作的兼容 ,最後附上Github連接RecyclerItemDecoration,歡迎star,fork。