RecyclerView之ItemDecoration详解

来源:互联网 发布:java工程师招聘北京 编辑:程序博客网 时间:2024/04/29 13:43

关于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:
这里写图片描述

<?xml version="1.0" encoding="utf-8"?><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>
<?xml version="1.0" encoding="utf-8"?><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来:
这里写图片描述

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来:
这里写图片描述

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都算在内     *     * <p>如果RecyclerView可以在两个维度滚动,那么调用者可能会传0给widthUsed和heightUsed</p>     *     * @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上边画一个圆形:
这里写图片描述

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就行了:
这里写图片描述

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);            }        }    }

这里写图片描述
我们再玩一个文本ItemDecoration:
这里写图片描述

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);    }}

什么?你觉得这个太简单了?那咱再玩一个。。更简单的:
这里写图片描述这里写图片描述

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

6 0
原创粉丝点击