起因
在項(xiàng)目開發(fā)中遇到了一些實(shí)際的需求驼鹅,為了滿足這些需求不得不去了解新的知識(shí)點(diǎn)或者加深對(duì)已知知識(shí)點(diǎn)的認(rèn)識(shí)塌计,現(xiàn)在就總結(jié)一下在實(shí)際開發(fā)中對(duì)RecyclerView的ItemDecoration的使用
ItemDecoration的原理
1.類的方法簡(jiǎn)介
這個(gè)類是RecyclerView的一個(gè)靜態(tài)內(nèi)部類,正如它的名字慷暂,它可以用來(lái)對(duì)RV的item做一些item之外裝飾槽袄,它只有定義了三個(gè)方法凡人,如下:
-
getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
這個(gè)方法用來(lái)設(shè)置RV的每個(gè)item的上下左右的間距,設(shè)置的值保存在outRect這個(gè)類中 -
public void onDraw(Canvas c, RecyclerView parent)
繪制方法坪哄,Canvas就是RV的Canvas质蕉,在調(diào)用RV的子View繪制之前被調(diào)用,所以在繪制子View之前繪制 -
public void onDrawOver( Canvas c, RecyclerView parent)
和onDraw方法一樣翩肌,只不過(guò)是在繪制完子View之后才被調(diào)用饰剥,所以可能會(huì)繪制在子View視圖之上
ItemDecoration只有這三個(gè)方法,和普通的View繪制一樣摧阅,它是依托于RecyclerView繪制子View的繪制周期來(lái)實(shí)現(xiàn)方法描述的這些功能的汰蓉,接著就來(lái)看看這三個(gè)方法被調(diào)用的時(shí)機(jī)
2.調(diào)用getItemOffsets()
以LinearLayoutManager為例,首先是測(cè)量棒卷,LinearLayoutManager布局子View時(shí)會(huì)調(diào)用layoutChunk方法顾孽,其中的measureChildWithMargins測(cè)量子View時(shí)會(huì)調(diào)用getItemDecorInsetsForChild方法,如下:
//==RV中==
void layoutChunk(){
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
比规。若厚。。蜒什。
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//累加所有的ItemDecoration的設(shè)置的offset
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//計(jì)算widthSpec和heightSpec测秸,getPaddingLeft() + getPaddingRight()
// + lp.leftMargin + lp.rightMargin + widthUsed用來(lái)確定在子View在AT——MOST
//模式下的最大寬高
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);
}
}
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
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);
//獲得保存在Rect中的上下左右的offset
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;
}
getItemDecorInsetsForChild方法將mItemDecorations中設(shè)置的間距累加并這個(gè)值保存在了RecyclerView.LayoutParams的mDecorInsets中
3.RecyclerView布局子View
前面在測(cè)量時(shí)將ItemDecoration設(shè)置的間距保存在了RecyclerView.LayoutParams的mDecorInsets中,在布局的時(shí)候就會(huì)用這個(gè)值來(lái)計(jì)算布局的偏移。layoutChunk方法中測(cè)量完了緊接著就是布局霎冯,如下
//假如豎直方向布局
void layoutChunk(){
铃拇。。沈撞。
measureChildWithMargins(view, 0, 0);
//getDecoratedMeasurement返回就是豎直整個(gè)子View包括ItemDecoration的所占用的高度
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
//左邊界慷荔,只是RV的padding
left = getPaddingLeft();
//右邊界包括子view的寬度、左右margin以及RV.LayoutParams的mDecorInsets
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view)缠俺;
//上邊界就是整體的偏移
top = layoutState.mOffset;
//下邊界包括子View的高度显晶、上下margin以及RV.LayoutParams的mDecorInsets
bottom = layoutState.mOffset + result.mConsumed;
} else {
。壹士。磷雇。
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom)
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
//真正布局確定子View的上下左右邊界時(shí)去除了margin和mDecorInsets
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
假設(shè)LinearLayoutManager豎直布局,在計(jì)算這個(gè)View需要多大的空間時(shí)是把View的margin和ItemDecoration設(shè)置的偏移全都算進(jìn)去了的躏救,但在真正布局子View的時(shí)候卻去除了子View的margin和mDecorInsets唯笙,這樣就預(yù)留了多的空間出來(lái)了。
4.onDraw() 和 onDrawOver()的調(diào)用
測(cè)量落剪、布局都已經(jīng)完成睁本,現(xiàn)在就剩繪制了,哪是怎樣控制這個(gè)先后順序的呢忠怖?
一般情況下ViewGroup的draw方法都是不會(huì)被調(diào)用的呢堰,但在給RV添加ItemDecoration的時(shí)候,會(huì)調(diào)用setWillNotDraw(false)來(lái)開啟ViewGroup的繪制凡泣,如下
public void addItemDecoration(ItemDecoration decor, int index) {
if (mItemDecorations.isEmpty()) {
//開啟ViewGroup的繪制方法
setWillNotDraw(false);
}
枉疼。。鞋拟。
}
@Override
public void draw(Canvas c) {
//第一步
super.draw(c);
//第三步
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
@Override
public void onDraw(Canvas c) {
//第二步
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
RV的draw方法首先是調(diào)用了super.draw(),ViewGroup的繪制會(huì)先調(diào)用自己的onDraw方法之后就把繪制事件分發(fā)給了子View骂维,最后才回到draw方法繼續(xù)執(zhí)行,所以ItemDecoration是利用了ViewGroup調(diào)用繪制方法的先后順序來(lái)達(dá)到目的的贺纲。
接下來(lái)就是實(shí)際的開發(fā)中需求
實(shí)現(xiàn)重疊的子View
項(xiàng)目中要求RV的圖片與圖片之間有重疊的部份航闺,在點(diǎn)擊某張圖片的時(shí)候?qū)D片完全顯示出來(lái),效果如下
ItemDecoration可以設(shè)置間距猴誊,但不影響子View的大小潦刃,間距為正數(shù)叫間距,間距為負(fù)數(shù)就是重疊懈叹,所以可以把Rect的top設(shè)置為負(fù)數(shù)就可以實(shí)現(xiàn)子View的重疊了乖杠。但還有一個(gè)問題,繪制都是從第一個(gè)子View開始挨個(gè)繪制的澄成,要讓點(diǎn)擊的子View全部顯示出來(lái)就需要改變繪制子View的順序胧洒,將點(diǎn)擊的View最后繪制畏吓,ViewGroup有一個(gè)getChildDrawingOrder,這個(gè)方法可以設(shè)置子View的繪制順序
/**
* Returns the index of the child to draw for this iteration. Override this
* if you want to change the drawing order of children. By default, it
* returns i.
* <p>
* NOTE: In order for this method to be called, you must enable child ordering
* first by calling {@link #setChildrenDrawingOrderEnabled(boolean)}.
*
* @param i The current iteration.
* @return The index of the child to draw this iteration.
*
* @see #setChildrenDrawingOrderEnabled(boolean)
* @see #isChildrenDrawingOrderEnabled()
*/
protected int getChildDrawingOrder(int childCount, int i) {
return i;
}
在RV中不用去復(fù)寫getChildDrawingOrder方法,它提供了RecyclerView.ChildDrawingOrderCallback卫漫,可以通過(guò)設(shè)置這個(gè)Callback來(lái)改變子View的繪制順序菲饼,最后大概的代碼如下
rv.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
View v = rv.getFocusedChild();
int focusIndex = rv.indexOfChild(v);
if (focusIndex == RecyclerView.NO_POSITION) {
return i;
}
// supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
// drawing order is 0 1 2 3 9 8 7 6 5 4
if (i < focusIndex) {
return i;
} else if (i < childCount - 1) {
return focusIndex + childCount - 1 - i;
} else {
return focusIndex;
}
}
});
rv.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
outRect.top = -200;
}
});
實(shí)現(xiàn)子View的間距不一樣
比如需求要求第一排的子View需要預(yù)留300px的空白空間用于顯示后面的海報(bào)汛兜,其他子View不變巴粪,但它們的類型一樣通今,一種解決方法就是設(shè)置ItemDecoration粥谬,把第一排的子view的top offset設(shè)置的大一些
rv.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
//第一排的子View設(shè)置top值
int pos = parent.getChildLayoutPosition(view);
if (pos < GRID_COLUMN_COUNT) {
outRect.top = 50;
}
}
關(guān)于ItemDecoration的總結(jié)就完了,最后看代碼時(shí)還發(fā)現(xiàn)用來(lái)拖拽子View的ItemTouchHelper居然是繼承自ItemDecoration辫塌,大概就是在onDraw方法中去改變子View的x漏策,y坐標(biāo)來(lái)實(shí)現(xiàn)的。