在上一篇文章中,我和大家一起简单讲解了关于RecyclerView的ItemDecoration抽象类的用法,不过既然文章名叫做《RecyclerView之ItemDecoration详解》,那么没有从源码的角度去分析实现原理显然是称不上”全”的。因此本篇文章我将带领大家在上篇文章的代码基础上改进,加入对GridLayoutManager的支持。
如果还没有看过我上一篇文章,请抓紧去阅读一下 RecyclerView之ItemDecoration详解(上)
由于GridLayoutManager的divider需要横线和竖线双向的绘制,所以比单纯的LinearLayoutManager复杂多了,一开始我也是拿来主义,首先去网上找了别人写的demo来进行分析,首先就是著名的【张鸿洋的博客】中的这篇文章Android RecyclerView 使用完全解析 体验艺术般的控件,相信很多人都看过,但是在我使用的过程中,我发现很多地方满足不了我的需求,比如九宫格类似的边界线的绘制等等,下面我带大家一起来看下我改进后的:
package com.binbin.divideritemdecoration;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
/**
* RecyclerView分割线
* 暂时对StaggeredGridLayoutManager错序不支持,其他情况均支持
* 自定义LayoutManager暂不做考虑
*/
public class DividerItemDecorationTest extends RecyclerView.ItemDecoration {
private static final String TAG = "tianbin";
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
private Drawable mDivider;
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
/**
* 设置是否显示边界线,即上下左右分割线
* 当为网格布局时上下左右均有效,当为线性布局时,只有上下或者左右分别有效
*/
private boolean drawBorderLine = false;
public DividerItemDecorationTest(Context context) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}
/**
* 自定义分割线
*
* @param context
* @param drawableId 分割线图片
*/
public DividerItemDecorationTest(Context context, @DrawableRes int drawableId) {
mDivider = ContextCompat.getDrawable(context, drawableId);
}
/**
* 垂直滚动,item宽度充满,高度自适应
* 水平滚动,item高度充满,宽度自适应
* 在itemView绘制完成之前调用,也就是说此方法draw出来的效果将会在itemView的下面
* onDrawOver方法draw出来的效果将叠加在itemView的上面
* @param c
* @param parent
* @param state
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
drawHorizontal(c, parent);
drawVertical(c, parent);
}
/**
* 滚动方向为垂直(VERTICAL_LIST),画水平分割线
* @param c
* @param parent
*/
public void drawVertical(Canvas c, RecyclerView parent) {
int spanCount = getSpanCount(parent);
int allChildCount = parent.getAdapter().getItemCount();
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
int top=0,bottom=0,left=0,right=0;
left = child.getLeft() - params.leftMargin;
right = child.getRight() + params.rightMargin;
if(drawBorderLine){
if(isFirstRaw(parent,params.getViewLayoutPosition(),spanCount)){
top=child.getTop()-params.topMargin-mDivider.getIntrinsicHeight();
bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}else{
if(isLastRaw(parent,params.getViewLayoutPosition(),spanCount,allChildCount)){
continue;
}
}
top = child.getBottom() + params.bottomMargin;
bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 滚动方向为水平,画垂直分割线
* @param c
* @param parent
*/
public void drawHorizontal(Canvas c, RecyclerView parent) {
int spanCount = getSpanCount(parent);
int allChildCount = 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();
int left=0,right=0,top=0,bottom=0;
top=child.getTop()-params.topMargin;
bottom=child.getBottom()+params.bottomMargin;
if(drawBorderLine){
//加上第一条
if(isFirstColumn(parent,params.getViewLayoutPosition(),spanCount)){
left=child.getLeft()-params.leftMargin-mDivider.getIntrinsicWidth();
right = left + mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}else{
if(isLastColum(parent,params.getViewLayoutPosition(),spanCount,allChildCount)){
continue;
}
}
left = child.getRight() + params.rightMargin;
right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* @param outRect
* @param view
* @param parent
* @param state
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int spanCount = getSpanCount(parent);
int childCount = parent.getAdapter().getItemCount();
int itemPosition=((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
boolean isLastRaw=isLastRaw(parent, itemPosition, spanCount, childCount);
boolean isLastColum=isLastColum(parent, itemPosition, spanCount, childCount);
boolean isFirstRaw=isFirstRaw(parent,itemPosition,spanCount);
boolean isFirstColumn=isFirstColumn(parent,itemPosition,spanCount);
int left=0,top=0,right=0,bottom=0;
//画线的规则是按照右边,下边,所以每个item默认只需设置右边,下边,边框处理按以下规则
right=mDivider.getIntrinsicWidth();
bottom=mDivider.getIntrinsicHeight();
if(drawBorderLine){
if(isFirstRaw){/**第一行:分为第一列和最后一列两种情况*/
top=mDivider.getIntrinsicHeight();
}
if(isFirstColumn){
left=mDivider.getIntrinsicWidth();
}
}else{
if(isLastColum){
right=0;//不画线,最后一列右边不留偏移
}
if(isLastRaw){
bottom=0;//不画线,最后一行底部不留偏移
}
}
outRect.set(left,top,right,bottom);
}
private boolean isFirstRaw(RecyclerView parent, int pos, int spanCount){
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation();
if (orientation == GridLayoutManager.VERTICAL) {
if(posreturn true;
}
}else{
if(pos%spanCount==0){
return true;
}
}
}else if(layoutManager instanceof LinearLayoutManager){
int orientation = ((LinearLayoutManager) layoutManager).getOrientation();
if (orientation == LinearLayoutManager.VERTICAL) {
if(pos==0){
return true;
}
}else{
//每一个都是第一行,也是最后一行
return true;
}
}
return false;
}
private boolean isFirstColumn(RecyclerView parent, int pos, int spanCount){
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation();
if (orientation == GridLayoutManager.VERTICAL) {
if(pos%spanCount==0){
return true;
}
}else{
if(posreturn true;
}
}
}else if(layoutManager instanceof LinearLayoutManager){
int orientation = ((LinearLayoutManager) layoutManager).getOrientation();
if (orientation == LinearLayoutManager.VERTICAL) {
//每一个都是第一列,也是最后一列
return true;
}else{
if(pos==0){
return true;
}
}
}
return false;
}
private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation();
if (orientation == GridLayoutManager.VERTICAL) {
//最后一列或者不能整除的情况下最后一个
if ((pos + 1) % spanCount == 0 /**|| pos==childCount-1*/){// 如果是最后一列
return true;
}
}else{
if(pos>=childCount-spanCount && childCount%spanCount==0){
//整除的情况判断最后一整列
return true;
}else if(childCount%spanCount!=0 && pos>=spanCount*(childCount/spanCount)){
//不能整除的情况只判断最后几个
return true;
}
// if(pos>=childCount-spanCount){
// return true;
// }
}
}else if(layoutManager instanceof LinearLayoutManager){
int orientation = ((LinearLayoutManager) layoutManager).getOrientation();
if (orientation == LinearLayoutManager.VERTICAL) {
//每一个都是第一列,也是最后一列
return true;
}else{
if(pos==childCount-1){
return true;
}
}
}
return false;
}
private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) {
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) {
int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation();
if (orientation == GridLayoutManager.VERTICAL) {
if(pos>=childCount-spanCount && childCount%spanCount==0){
//整除的情况判断最后一整行
return true;
}else if(childCount%spanCount!=0 && pos>=spanCount*(childCount/spanCount)){
//不能整除的情况只判断最后几个
return true;
}
// if(pos>=childCount-spanCount){
// return true;
// }
}else{
//最后一行或者不能整除的情况下最后一个
if ((pos + 1) % spanCount == 0 /**|| pos==childCount-1*/){// 如果是最后一行
return true;
}
}
}else if(layoutManager instanceof LinearLayoutManager){
int orientation = ((LinearLayoutManager) layoutManager).getOrientation();
if (orientation == LinearLayoutManager.VERTICAL) {
if(pos==childCount-1){
return true;
}
}else{
//每一个都是第一行,也是最后一行
return true;
}
}
return false;
}
private int getSpanCount(RecyclerView parent) {
// 列数
int spanCount = -1;
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount();
}
return spanCount;
}
public boolean isDrawBorderLine() {
return drawBorderLine;
}
public void setDrawBorderLine(boolean drawBorderLine) {
this.drawBorderLine = drawBorderLine;
}
}
整体思路:首先去处理getItemOffsets,按照right,bottom偏移的规则,去留出空隙,然后处理横向和竖向两种情况,分别判断出首行,末行,首列,末列,根据是否绘制边界线(drawBorderLine)来进行处理偏移,这样得出的效果如图:
咦?怎么会这样呢。。。这里就涉及到交叉处的处理,因为没有加上divider的宽或者高,所以交叉的地方没有被绘制(RecyclerView默认背景色就是交叉处的那个红色),其实如果divider很细,是看不出来这个bug的,但是我们要追求完美,所以解决方案如下:只需要在drawVertical或者drawHorizontal的其中一个加入mDivider的宽或高即可(如果两个里面都加的话就会出现重叠绘制交叉处的bug)
再来运行,效果如下:
怎么样?看上去还不错吧。。。下面是无边框的情况(红色处是背景色):
这个时候心里终于舒坦了,完美解决,但仔细看发现好像有什么地方不对,每个item的宽度不是均分的,为了验证,把item的宽度写死,运行效果果然证实了我的猜想,效果如下:
没错,果然没有充满,背景色露出来了。。。这是怎么回事?继续寻找原因!
猜想是不是绘制的时候计算错误呢,于是屏蔽掉那块代码,只留出偏移,发现仍然如此,当我把偏移也屏蔽掉的时候发现item均分了,于是我猜想是在计算偏移的时候出了问题,跟踪到源码里去查看:发现调用getItemOffsets的地方只有一个getItemDecorInsetsForChild(View child):
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;
}
发现我们所计算的偏移被加入到了insets中,而调用这个函数的地方我们发现有三个,分别是:
我们发现,第一个暂时没有被调用的地方,第二个是被LinearLayoutManager调用的,暂且不管,最后一个是被GridLayoutManager中的layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result)方法调用的,而这个方法是用来layout每个item的,下面来分析这个方法:
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
final int otherDirSpecMode = mOrientationHelper.getModeInOther();
final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
// if grid layout's dimensions are not specified, let the new row change the measurements
// This is not perfect since we not covering all rows but still solves an important case
// where they may have a header row which should be laid out according to children.
if (flexibleInOtherDir) {
updateMeasurements(); // reset measurements
}
final boolean layingOutInPrimaryDirection =
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
int count = 0;
int consumedSpanCount = 0;
int remainingSpan = mSpanCount;
if (!layingOutInPrimaryDirection) {
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
remainingSpan = itemSpanIndex + itemSpanSize;
}
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
int pos = layoutState.mCurrentPosition;
final int spanSize = getSpanSize(recycler, state, pos);
if (spanSize > mSpanCount) {
throw new IllegalArgumentException("Item at position " + pos + " requires " +
spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ " spans.");
}
remainingSpan -= spanSize;
if (remainingSpan < 0) {
break; // item did not fit into this row or column
}
View view = layoutState.next(recycler);
if (view == null) {
break;
}
consumedSpanCount += spanSize;
mSet[count] = view;
count++;
}
if (count == 0) {
result.mFinished = true;
return;
}
int maxSize = 0;
float maxSizeInOther = 0; // use a float to get size per span
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
for (int i = 0; i < count; i++) {
View view = mSet[i];
if (layoutState.mScrapList == null) {
if (layingOutInPrimaryDirection) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (layingOutInPrimaryDirection) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
calculateItemDecorationsForChild(view, mDecorInsets);
measureChild(view, otherDirSpecMode, false);
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) /
lp.mSpanSize;
if (otherSize > maxSizeInOther) {
maxSizeInOther = otherSize;
}
}
if (flexibleInOtherDir) {
// re-distribute columns
guessMeasurement(maxSizeInOther, currentOtherDirSize);
// now we should re-measure any item that was match parent.
maxSize = 0;
for (int i = 0; i < count; i++) {
View view = mSet[i];
measureChild(view, View.MeasureSpec.EXACTLY, true);
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
}
}
// Views that did not measure the maxSize has to be re-measured
// We will stop doing this once we introduce Gravity in the GLM layout params
for (int i = 0; i < count; i ++) {
final View view = mSet[i];
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Rect decorInsets = lp.mDecorInsets;
final int verticalInsets = decorInsets.top + decorInsets.bottom
+ lp.topMargin + lp.bottomMargin;
final int horizontalInsets = decorInsets.left + decorInsets.right
+ lp.leftMargin + lp.rightMargin;
final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
final int wSpec;
final int hSpec;
if (mOrientation == VERTICAL) {
wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
horizontalInsets, lp.width, false);
hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
View.MeasureSpec.EXACTLY);
} else {
wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets,
View.MeasureSpec.EXACTLY);
hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
verticalInsets, lp.height, false);
}
measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
}
}
result.mConsumed = maxSize;
int left = 0, right = 0, top = 0, bottom = 0;
if (mOrientation == VERTICAL) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = bottom - maxSize;
} else {
top = layoutState.mOffset;
bottom = top + maxSize;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = right - maxSize;
} else {
left = layoutState.mOffset;
right = left + maxSize;
}
}
for (int i = 0; i < count; i++) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// 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);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.isFocusable();
}
Arrays.fill(mSet, null);
}
这个方法虽然很长,但主要的功能就是用来measure和layout的,跟我们常见的自定义view流程是一样的。我们看主要的部分,首先是第69行,找到这个方法:
/**
* Calculates the item decor insets applied to the given child and updates the provided
* Rect instance with the inset values.
*
* - The Rect's left is set to the total width of left decorations.
* - The Rect's top is set to the total height of top decorations.
* - The Rect's right is set to the total width of right decorations.
* - The Rect's bottom is set to total height of bottom decorations.
*
*
* Note that item decorations are automatically calculated when one of the LayoutManager's
* measure child methods is called. If you need to measure the child with custom specs via
* {@link View#measure(int, int)}, you can use this method to get decorations.
*
* @param child The child view whose decorations should be calculated
* @param outRect The Rect to hold result values
*/
public void calculateItemDecorationsForChild(View child, Rect outRect) {
if (mRecyclerView == null) {
outRect.set(0, 0, 0, 0);
return;
}
Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
outRect.set(insets);
}
这是RecyclerView中的一个方法,里面调用了上面我们提到的getItemDecorInsetsForChild,这里就是用来计算我们设置的偏移,并传入到GridLayoutManager中的layoutChunk方法中,去用来测量每个item加上偏移值和padding margin之后的大小,网上有人说这个值算入了padding中,但我感觉并没有,而且官方也解释了这个值的含义:
Retrieve any offsets for the given item. Each field of outRect
specifies the number of pixels that the item view should be inset by, similar to padding or margin.The default implementation sets the bounds of outRect to 0 and returns
只是一个与padding和margin相似的东西,并不能算入任何一个当中。
在完成测量之后,看第164行,进而又对item进行了layout,跟踪进去:
/**
* Lay out the given child view within the RecyclerView using coordinates that
* include any current {@link ItemDecoration ItemDecorations} and margins.
*
* LayoutManagers should prefer working in sizes and coordinates that include
* item decoration insets whenever possible. This allows the LayoutManager to effectively
* ignore decoration insets within measurement and layout code. See the following
* methods:
*
* - {@link #layoutDecorated(View, int, int, int, int)}
* - {@link #measureChild(View, int, int)}
* - {@link #measureChildWithMargins(View, int, int)}
* - {@link #getDecoratedLeft(View)}
* - {@link #getDecoratedTop(View)}
* - {@link #getDecoratedRight(View)}
* - {@link #getDecoratedBottom(View)}
* - {@link #getDecoratedMeasuredWidth(View)}
* - {@link #getDecoratedMeasuredHeight(View)}
*
*
* @param child Child to lay out
* @param left Left edge, with item decoration insets and left margin included
* @param top Top edge, with item decoration insets and top margin included
* @param right Right edge, with item decoration insets and right margin included
* @param bottom Bottom edge, with item decoration insets and bottom margin included
*
* @see View#layout(int, int, int, int)
* @see #layoutDecorated(View, int, int, int, int)
*/
public void layoutDecoratedWithMargins(View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
我们可以看到在测量和布局的过程中,均对我们设置的偏移值进行了加入并处理,并且每个item所占大小是包含了padding、margin、insets三个值的,由于我们设置偏移的时候并没有去进行均分处理,所以造成了上面出现的bug。找到原因了,就好解决了!下面给出解决方案:
在getItemOffsets中(保证每个Item的insets.left+insets.right相等,这样才能达到均分的目的)
以我们的demo为例(手机为1080*1920):有边界总偏移值为5dp*4=20dp=60px;每个item的左右偏移总和为60/3=20px
Column | Left | Right |
---|---|---|
0 | 15 | 5 |
1 | 10 | 10 |
2 | 5 | 15 |
GridLayoutManager.SpanSizeLookup spanSizeLookup ((GridLayoutManager)layoutManager).getSpanSizeLookup();
//左边的跨度索引值[0,spanCount)之间
int spanIndexLeft = spanSizeLookup.getSpanIndex(itemPosition, spanCount);
//右边的跨度索引值[0,spanCount)之间
int spanIndexRight = spanIndexLeft - 1 + spanSizeLookup.getSpanSize(itemPosition);
if(drawBorderLine){
left=dividerWidth * (spanCount - spanIndexLeft) / spanCount;
right=dividerWidth * (spanIndexRight + 1) / spanCount;
}else{
left = dividerWidth * spanIndexLeft / spanCount;
right = dividerWidth * (spanCount - spanIndexRight - 1) / spanCount;
}
我们可以根据SpanSizeLookup去计算偏移值,这样也支持了不同spanSize的item,而判断首末行列的方法也要根据这个来进行变化。修改后的效果如下:
另外我加入了不同构造方法,支持传入drawable和颜色值,直接设置divider样式,并且加入了drawBorderLeftAndRight和drawBorderTopAndBottom两个变量分别去控制左右或者上下边界线的绘制。同时把LinearLayoutManager和GridLayoutManager加入支持,还有onlySetItemOffsetsButNoDraw变量去控制只留偏移不画线,横向和纵向布局都支持自动识别,这样就打造了一个万能的ItemDecoration
注:使用过程中避免使用小数dp值,因为getItemOffsets中在做均分的时候小数会出现不能整除的情况,而Rect不支持小数,所以这里算是追求完美的过程中留下的一个遗憾吧,也可能我没有想到好的方法,希望有解决方案的朋友可以留言。。。
好了,今天的讲解到此结束,有疑问的朋友请在下面留言。