RecyclerView之ItemDecoration详解

关于RecyclerView的ItemView装饰,之前一直用官方Demo的DividerItemDecoration,并没有认真地去理解ItemDecoration的用法,也没能体会到ItemDecoration的强大,直到要用到横向的RecyclerView,而且最左边的和最右边的Item要留出间隔(虽然clip结合padding可以实现),才认真地理解一下ItemDecoration
RecyclerView可以多次调用addItemDecoration(ItemDecoration decor)addItemDecoration(ItemDecoration decor, int index)方法有序地为RecyclerView添加ItemDecoration,ItemDecoration会影响每一个ItemView的测量和绘制。
先看一下不加ItemDecoration时的RecyclerView:
RecyclerView之ItemDecoration详解_第1张图片


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.frank.lollipopdemo.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="100px"
        android:layout_height="500px"
        android:background="#CCCCCC" />

RelativeLayout>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100px"
    android:layout_height="100px"
    android:background="#99FFFF00"
    android:gravity="center"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/iv_logo"
        android:layout_width="50px"
        android:layout_height="50px" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"/>

LinearLayout>

为了方便,这里尺寸都使用px,建议使用dp。

RecyclerView宽100px高500px,背景为灰色#CCCCCC。每个ItemView宽100px高100px,背景为黄色#99FFFF00。
ItemDecorationRecyclerView的静态内部类,用来向ItemView绘制一些装饰以及调整ItemView的偏移。它有只有三个方法:

  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
  • onDraw(Canvas c, RecyclerView parent, State state)
  • onDrawOver(Canvas c, RecyclerView parent, State state)

getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)方法,直观一点说,就是用来设置ItemView的inset(内嵌偏移)的,类似于InsetDrawable,可以看成在ItemView的外面包裹一层偏移。
我们先让每个ItemView下面空出50px来:
RecyclerView之ItemDecoration详解_第2张图片

public class ItemDecor extends RecyclerView.ItemDecoration {

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 0, 0, 50);
    }

}

让它上下左右都空出50px来:
RecyclerView之ItemDecoration详解_第3张图片

public class ItemDecor extends RecyclerView.ItemDecoration {

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(50, 50, 50, 50);
    }

}

可以看到,我们通过设置outRect的left, top, right, bottom属性值就可以让ItemView产生相应的偏移(内嵌),那RecyclerView是怎么根据outRect的这四个属性值设置ItemView的inset的呢?
RecyclerView是一个自定义的ViewGroup,每个ItemView都是它的child,那它又是怎样通过LayoutManager测量并布局ItemView的呢?
看一下LayoutManagermeasureChildWithMargins方法(measureChild方法与之类似,只是没有ItemView的margin而已):

/**
     * 使用标准测量策略测量ItemView,
     * 把RecyclerView的padding、所有已经添加的ItemDecoration尺寸、ItemView的margin都算在内
     *
     * 

如果RecyclerView可以在两个维度滚动,那么调用者可能会传0给widthUsed和heightUsed

* * @param child 要测量的子view(ItemView) * @param widthUsed 已经被其它ItemDecoration占用的宽度(px) * @param heightUsed 已经被其它ItemDecoration占用的高度(px) */
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); // 累加当前ItemDecoration宽高值 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); } }

可以看到测量child时,需要调用RecyclerViewgetItemDecorInsetsForChild(View child)方法获得ItemDecoration的inset:

 Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        // 如果不是dirty数据,直接返回
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }
        // 如果是dirty数据,重新计算
        final Rect insets = lp.mDecorInsets;
        // 重置inset
        insets.set(0, 0, 0, 0);
        //将inset设置为所有已添加的ItemDecoration的getItemOffsets的累加(因为RecyclerView可能添加了多个ItemDecoration)
        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;
    }

第15行,调用了ItemDecorationgetItemOffsets(Rect outRect, View view, RecyclerView parent, State state)方法,所以我们在getItemOffsets()方法中对outRect的设置会被当做ItemView的inset进行测量,inset就像padding和margin一样,会影响view的尺寸和位置。
好了,我们大概知道了我们对outRect的设置是怎样对ItemView产生影响的(RecyclerView和LayoutManager共同协调测量ItemView的逻辑有点复杂,有时间要认真看一下),接下来,我们就可以利用outRect随便调整ItemView的位置,就像我们平时自定义ViewGroup时写onLayout()方法layout子View一样,是不是很灵活啊。
然后就是绘制ItemDecoration了。onDraw()的绘制会先于ItemView的绘制,所以如果你在onDraw()方法中绘制的东西在ItemView边界内,就会被ItemView盖住。而onDrawOver()会在ItemView绘制之后再绘制,所以如果你在onDrawOver()方法中绘制的东西在ItemView边界内,就会盖住ItemView。简单点说,就是先执行ItemDecoration的onDraw()、再执行ItemView的onDraw()、再执行ItemDecoration的onDrawOver()。由于和RecyclerView使用的是同一个Canvas,所以你想在Canvas上画什么都可以,就像我们平时自定义View时写onDraw()方法一样。
我们先在RecyclerView上边画一个圆形:
RecyclerView之ItemDecoration详解_第4张图片

public class ItemDecor extends RecyclerView.ItemDecoration {

    Paint mPaint;

    public ItemDecor() {
        mPaint = new Paint();
        mPaint.setColor(0x99FF0000);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        c.drawCircle(50, 30, 30, mPaint);
    }

}

只需要在Canvas中你想要绘制的位置绘制你想画的东西就行了,什么矩形、渐变、Bitmap等等,没有做不到只有你想不到。
那我想绘制分隔线,怎么知道每个ItemView的位置呢?很简单,遍历一下RecyclerView的child就行了:
RecyclerView之ItemDecoration详解_第5张图片

public class ItemDecor extends RecyclerView.ItemDecoration {

    Paint mPaint;

    public ItemDecor() {
        mPaint = new Paint();
        mPaint.setColor(0x99FF0000);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        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 + 50;
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 0, 0, 50);
    }
}

如果想让第一个ItemView之前也有一个红色分割线怎么办?也很简单,先给第一个ItemView设置insetTop和insetBottom内嵌,其它的ItemView只设置insetBottom内嵌:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int itemPosition = parent.getChildAdapterPosition(view);
        int dataSize = parent.getAdapter().getItemCount();
        if (itemPosition == 0) {
            outRect.set(0, 50, 0, 50);
        } else {
            outRect.set(0, 0, 0, 50);
        }
    }

然后绘制的时候,在第一个ItemView的insetTop区域再绘制一个分割线就行了:

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        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 top1 = child.getTop() - params.bottomMargin -
                    Math.round(ViewCompat.getTranslationY(child)) - 50;
            final int bottom1 = top1 + 50;
            final int top2 = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));
            final int bottom2 = top2 + 50;
            c.drawRect(left, top2, right, bottom2, mPaint);
            if (i == 0) {
                c.drawRect(left, top1, right, bottom1, mPaint);
            }
        }
    }

RecyclerView之ItemDecoration详解_第6张图片
我们再玩一个文本ItemDecoration:
RecyclerView之ItemDecoration详解_第7张图片

public class ItemDecor extends RecyclerView.ItemDecoration {

    Paint mPaint;

    public ItemDecor() {
        mPaint = new Paint();
        mPaint.setColor(0x99FF0000);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        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.getTop() - params.bottomMargin -
                    Math.round(ViewCompat.getTranslationY(child)) - 30;
            mPaint.setTextSize(20f);
            final int adapterPosition = parent.getChildAdapterPosition(child);
            c.drawText("item:" + adapterPosition, left + 5, top + 20, mPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 30, 0, 0);
    }
}

什么?你觉得这个太简单了?那咱再玩一个。。更简单的:
RecyclerView之ItemDecoration详解_第8张图片RecyclerView之ItemDecoration详解_第9张图片

public class SkillRatingDistributionItemDecor extends RecyclerView.ItemDecoration {

    private Paint mPaint;
    private Paint mValuePaint;
    private PathEffect mDashPathEffect;

    public SkillRatingDistributionItemDecor() {
        mPaint = new Paint();
        mValuePaint = new Paint();
        mDashPathEffect = new DashPathEffect(new float[]{5, 5, 5, 5}, 1);
        mPaint.setAntiAlias(true);
        mValuePaint.setAntiAlias(true);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int rv_left = parent.getLeft();
        final int rv_top = parent.getTop();
        final int rv_right = parent.getRight();
        final int rv_bottom = parent.getHeight();
        final int rv_top_line = rv_top + 42;
        final int rv_bottom_line = rv_bottom - 20;
        final int y_spacing = (int) ((rv_bottom_line - rv_top_line) / 4f);
        for (int i = 0; i < 5; i++) {
            if (i == 0 || i == 4) {
                mPaint.setPathEffect(null);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setColor(0xFFE1E6EB);
                mPaint.setStrokeWidth(2f);
            } else {
                mPaint.setPathEffect(null);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setColor(0x7FE1E6EB);
                mPaint.setStrokeWidth(1f);
            }
            c.drawLine(rv_left, rv_top_line + i * y_spacing, rv_right, rv_top_line + i * y_spacing, mPaint);
        }
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            SkillRatingDistributionObj skillRatingDistributionObj = (SkillRatingDistributionObj) child.getTag();
            if (skillRatingDistributionObj != null) {
                int skill_rating = Integer.parseInt(skillRatingDistributionObj.getSkill_rating());
                if (skill_rating % 25 != 0 && skill_rating != 1) {
                    continue;
                }
                final int child_left = child.getLeft();
                mValuePaint.setColor(0xFF000000);
                mValuePaint.setTextSize(18f);
                mValuePaint.setTextAlign(Paint.Align.CENTER);
                c.drawText(skillRatingDistributionObj.getSkill_rating(), child_left + 8, 30f, mValuePaint);
                mPaint.setPathEffect(mDashPathEffect);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setColor(0xFFE1E6EB);
                mPaint.setStrokeWidth(2f);
                c.drawLine(child_left + 8, rv_top_line, child_left + 8, rv_bottom_line, mPaint);
            }
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int itemPosition = parent.getChildAdapterPosition(view);
        int dataSize = parent.getAdapter().getItemCount();
        if (itemPosition == 0) {
            outRect.set(20, 42, 0, 22);
        } else if (itemPosition == dataSize - 1) {
            outRect.set(0, 42, 20, 22);
        } else {
            outRect.set(0, 42, 0, 22);
        }
    }
}

我们可以通过outRect控制ItemView上下左右的“偏移”,可以通过onDraw随便往RecyclerView/ItemView中画东西,简直不能再灵活啊。RecyclerView类由于大量的静态内部类,代码行数一万多行了,和LayoutManager类的交互也很复杂,有时间研究一下代码可以学到很多东西。
文章中的代码已经很完整了,如果想直接看一下demo,可以clone一下我在Git上的Demo。


ListView:
如果ListView高度为wrap_content,那么无论Item总高度多少,都不会在底部添加分隔线。
如果ListView高度为match_parent或固定高度,那么当Item总高度小于ListView高度时会添加底部分隔线,否则不会添加底部的分割线。
android:headerDividersEnabledandroid:footerDividersEnabled只能决定ListView的HeaderView和FooterView分隔线是否绘制(默认为true绘制),并不能消除分隔线导致的Item偏移,即HeaderView/FooterView底部分隔线的空间始终存在,如果设置为false只是不会绘制分割线样式而已(每一个HeaderView/FooterView其实都是一个ItemView)。


Thanks:
Piasy' blog

你可能感兴趣的:(Android)