RecyclerView定制:通用ItemDecoration及全展开RecyclerView的实现

Android L面世以后,Google就推荐在开发项目中使用RecyclerView来取代ListView,由于RecyclerView的灵活性跟性能都要比ListView更强,可是,带来的问题也很多,好比:列表分割线都要开发者本身控制,再者,RecyclerView的测量与布局的逻辑都委托给了本身LayoutManager来处理,若是须要对RecyclerView进行改造,相应的也要对其LayoutManager进行定制。本文主要就以如下场景给出RecyclerView使用参考:javascript

RecyclerView的几种经常使用场景

  • 如何实现带分割线的列表式RecyclerView
  • 如何实现带分割线网格式RecyclerView
  • 如何实现全展开的列表式RecyclerView(好比:嵌套到ScrollView中使用)
  • 如何实现全展开的网格式RecyclerView(好比:嵌套到ScrollView中使用)

先看一下实现样式,为了方便控制,边界的均不设置分割线,方便定制,若是须要能够采用Padding或者Margin来实现。 Github链接RecyclerItemDecorationjava

网格式列表样式

全展开的网格式列表

全展开的线性列表

不一样场景RecyclerView实现

默认的纵向列表式RecyclerView

首先看一下最简单的纵向线性RecyclerView,通常用如下代码:git

LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
    linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(linearLayoutManager);复制代码

以上就是最简单的线性RecyclerView的实现,但默认不带分割线,若是想要使用好比20dp的黑色做为分割线,就须要本身定制,Google为RecyclerView提供了ItemDecoration,它的做用就是为Item添加一些附属信息,好比:分割线,浮层等。github

带分割线的列表式RecyclerView--LinearItemDecoration

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函数获取所需空间信息,源码以下:app

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函数:ide

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--GridLayoutItemDecoration

网格式RecyclerView的处理流程跟上面的线性列表相似,不过网格式的须要根据每一个Item的位置为其设置好边距,好比最左面的不须要左边占位,最右面的不须要右面的占位,最后一行不须要底部的占位,以下图所示ui

网格式ItemDocration的限制

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--ExpandedLinearLayoutManager

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 = measureHeight + getPaddingBottom() + getPaddingTop();
    measureWidth = measureWidth + getPaddingLeft() + getPaddingRight();
    measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight;
    measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth;
    if (getOrientation() == HORIZONTAL && measureWidth > widthSize) {
        if (widthMode == View.MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(measureWidth, measureHeight);
        } else {
            super.onMeasure(recycler, state, widthSize, heightSpec);
        }
    } else if (getOrientation() == VERTICAL && measureHeight > heightSize) {
        if (heightMode == View.MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(measureWidth, measureHeight);
        } else {
            setMeasuredDimension(measureWidth, heightSize);
        }
    } else {
        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--ExpandedGridLayoutManager

全展开的网格式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 = measureHeight + getPaddingBottom() + getPaddingTop();
    measureWidth = measureWidth + getPaddingLeft() + getPaddingRight();
    measureHeight = heightMode == View.MeasureSpec.EXACTLY ? heightSize : measureHeight;
    measureWidth = widthMode == View.MeasureSpec.EXACTLY ? widthSize : measureWidth;
    if (getOrientation() == HORIZONTAL && measureWidth > widthSize) {
        if (widthMode == View.MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(measureWidth, measureHeight);
        } else {
            super.onMeasure(recycler, state, widthSpec, heightSpec);
        }
    } else if (getOrientation() == VERTICAL && measureHeight > heightSize) {
        if (heightMode == View.MeasureSpec.UNSPECIFIED) {
            setMeasuredDimension(measureWidth, measureHeight);
        } else {
            super.onMeasure(recycler, state, widthSpec, heightSpec);
        }
    } else {
        setMeasuredDimension(measureWidth, measureHeight);
    }
}复制代码

最后附上横向滑动效果图:

横向滑动

以上就是比较通用的RecyclerView使用场景及所作的兼容 ,最后附上Github连接RecyclerItemDecoration,欢迎star,fork。

相关文章
相关标签/搜索