1.看過第一篇文章的都會注意到在設(shè)置RecyclerView的GridLayoutManager的Item時設(shè)置的是正方形的布局×墩龋可是當(dāng)設(shè)置自定義分割線的時候出現(xiàn)了一些問題。如果對GridLayoutManager添加分割線有疑惑的可以查看上一篇文章盗迟。
RecyclerView添加自定義ItemDecoration實現(xiàn)(1)
具體情況如下圖所示:
問題:從圖中可能不能很好的看出存在的問題坤邪。我們不妨加大分割線的寬度之后再觀察。所以我們將之前分割線的寬度改為20dp罚缕。效果如下:
現(xiàn)在可以很明顯的看出之前遺留下來的bug:第一列和第二列的形狀和第三列的不同艇纺。那么為什么會出現(xiàn)這種情況呢?我們下面從源碼分析一下添加ItemDecoration的原理邮弹。
2.在RecyclerView中有如下一部分代碼
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView and any added item decorations into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child Child view to measure
* @param widthUsed Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
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);
}
}
/**
* Calculate a MeasureSpec value for measuring a child view in one dimension.
*
* @param parentSize Size of the parent view where the child will be placed
* @param parentMode The measurement spec mode of the parent
* @param padding Total space currently consumed by other elements of parent
* @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
* Generally obtained from the child view's LayoutParams
* @param canScroll true if the parent RecyclerView can scroll in this dimension
*
* @return a MeasureSpec value for the child view
*/
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (canScroll) {
if (childDimension == LayoutParams.MATCH_PARENT){
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
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);
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;
}
- 首先我們先看 getChildMeasureSpec 方法黔衡。 當(dāng) childDimension >= 0 時讓結(jié)果為 childDimension,否則為 Math.max(0, parentSize - padding)腌乡。這個的意思就是如果子view的尺寸如果是精確值的話盟劫,那么就是精確值的結(jié)果,如果是 MATCH_PARENT 或者 WRAP_CONTENT 的話就開始計算結(jié)果与纽。
-
其次我們從 measureChild 方法中可以看出侣签,widthSpec 的 getChildMeasureSpec 中可以得到
paddingg = getPaddingLeft() + getPaddingRight() + widthUsed;
而
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); widthUsed += insets.left + insets.right;
-
最后我們可以從 getItemDecorInsetsForChild 方法中可以看出 Rect insets就是在自定義 ItemDecoration 時
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {} 中的outRect。所以計算 widthUsed 時候渣锦,與 outRect 有關(guān)硝岗,而我們在 getItemOffsets 方法中有這么一段代碼if (!isLastColumn(currentItemPosition, mSpanCount)) { outRect.right = mDrawable.getIntrinsicWidth(); } else { outRect.right = 0; }
如果是最后一列的話,outRect.right = 0袋毙;這使得第三列子view的 widthUsed 與前兩列的不同型檀,所以第三列子view的 padding 就會比前兩列的少一個分割線的寬度。
3.解決
那我們應(yīng)該怎么解決這個問題呢听盖?問題的關(guān)鍵就是分割線的寬度分配不均導(dǎo)致的胀溺,我們只要均勻分配寬度即可。
如圖所示皆看,我們對于有三列的情況來說仓坞,averWidth = (2 * dividerWidth)/3, 將兩個分割線的寬度,均分給3個子 view 的 outRect腰吟。所以為了適配其他情況得到如下式子 :
averWidth = [(spanCount - 1 ) * dividerWidth ]/spanCount ;
現(xiàn)在是最重要的步驟无埃,如果將averWidth分給每個子 view徙瓶,就會存在如下情況:
leftN + rightN = averWidth;
left(N+1) + rigthN = dividerWidth;
(注:leftN為第N個view的左偏移值,rightN為第N個View的右偏移值,計數(shù)從0開始)
對于 rightN 存在如下的情況:
right0 = averWidth - left0;
right1 = averWidth - left1;
right2 = averWidth - left2;
.
.
.
rightN = averWidth - leftN;
對于 leftN 存在如下的情況:
left0 = 0;
left1 = dividerWidth - right0;
left2 = dividerWidth - right1;
.
.
.
leftN = dividerWidth - right(N-1);
從上面的式子可以看出嫉称,我們只要得到 leftN 的就能很快的得到 rightN侦镇,所以將 rightN 式子合并到 leftN 中得到如下的結(jié)果:
left0 = 0;
left1 = dividerWidth - (averWidth - left0)
= dividerWidth - averWidth + left0
= dividerWidth - averWidth
left2 = dividerWidth - (averWidth - left1)
= dividerWidth -[averWidth - (dividerWidth - averWidth)]
= 2 * (dividerWidth - averWidth)
.
.
.
leftN = N * (dividerWidth - averWidth)
所以,根據(jù)化簡后的結(jié)果更改代碼织阅,修改完后的 getItemOffsets 方法具體如下:
/**
* @param outRect 用于規(guī)定分割線的范圍
* @param view 進(jìn)行分割線操作的子view
* @param parent 父view
* @param state (這里暫時不使用)
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
//notification的時候要獲取
mTotalItems = parent.getAdapter().getItemCount();
if (0 == mSpanCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
//判斷是否為GridLayoutManager
if (layoutManager instanceof GridLayoutManager) {
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
mSpanCount = gridLayoutManager.getSpanCount();
} else {
mSpanCount = 1;
}
}
//在源碼中有一個過時的方法壳繁,里面有獲取當(dāng)前ItemPosition
int currentItemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
//
if (!isLastRow(currentItemPosition, mTotalItems, mSpanCount))
outRect.bottom = mDrawable.getIntrinsicWidth();
else
outRect.bottom = 0;
//將isLastColumn注釋,添加下面代碼
// if (!isLastColumn(currentItemPosition, mSpanCount)) {
// outRect.right = mDrawable.getIntrinsicWidth();
// } else {
// outRect.right = 0;
// }
int averWidth = (mSpanCount - 1) * mDrawable.getIntrinsicWidth() / mSpanCount;
int dX = mDrawable.getIntrinsicWidth() - averWidth;
outRect.left = (currentItemPosition % mSpanCount) * dX;
outRect.right = averWidth - outRect.left;
}
結(jié)果截圖: