Android控件——RecyclerView系列三(Item Decoration)

1 ItemDecoration

1.1 ItemDecoration简单使用

前述文章的例子item间并没有分割线,RecyclerView并没有支持divider这样的属性,我们可以自己定制分割线。

RecyclerView添加分割线的方法是:mRecyclerView.addItemDecoration(),该方法的参数为RecyclerView.ItemDecoration,该类为抽象类

该类的源码为:

public abstract static class ItemDecoration {
        public ItemDecoration() {
        }

        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDraw(c, parent);
        }

		/** @deprecated */
        @Deprecated
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDrawOver(c, parent);
        }

		/** @deprecated */
        @Deprecated
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        /** @deprecated */
        @Deprecated
        public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
        }
    }

当我们调用addItemDecoration()方法添加decoration的时候,RecyclerView在绘制的时候,去会绘制decorator,即调用该类的onDraw()onDrawOver()方法,

  • onDraw方法先于drawChildren
  • onDrawOver在drawChildren之后,一般我们选择复写其中一个即可。
  • getItemOffsets为每一个item设置一定的偏移量,主要用于绘制Decorator。

简单示例(这里使用LayoutManager为LinearLayoutManager时)

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent) {

        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }

    }


    public void drawVertical(Canvas c, RecyclerView parent) {
        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);
            android.support.v7.widget.RecyclerView v = new android.support.v7.widget.RecyclerView(parent.getContext());
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        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 left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

该实现类可以看到通过读取系统主题中的 android.R.attr.listDivider作为Item间的分割线,并且支持横向和纵向

之后在原来的代码中添加一句:

mRecyclerView.addItemDecoration(new DividerItemDecoration(this,
DividerItemDecoration.VERTICAL_LIST));

onDraw()是怎么被调用的呢?还有ItemDecoration还有一个方法onDrawOver(),该方法也可以被重写,那么onDraw()和onDrawOver()之间有什么关系呢?

class RecyclerView extends ViewGroup{
     public void draw(Canvas c) {
         super.draw(c); //调用View的draw(),该方法会先调用onDraw(),再调用dispatchDraw()绘制children

         final int count = mItemDecorations.size();
         for (int i = 0; i < count; i++) {
             mItemDecorations.get(i).onDrawOver(c, this, mState);
         }
         ...
     }
     public void onDraw(Canvas c) {
         super.onDraw(c);
         final int count = mItemDecorations.size();
         for (int i = 0; i < count; i++) {
             mItemDecorations.get(i).onDraw(c, this, mState);
         }
     }
 }

根据View的绘制流程,首先调用RecyclerView重写的draw()方法,随后super.draw()即调用View的draw(),该方法会先调用onDraw()(这个方法在RecyclerView重写了),再调用dispatchDraw()绘制children。因此:ItemDecoration的onDraw()在绘制Item之前调用,ItemDecoration的onDrawOver()在绘制Item之后调用。

该分割线是系统默认的,你可以在theme.xml中找到该属性的使用情况。那么,使用系统的listDivider有什么好处呢?就是方便我们去随意的改变,该属性我们可以直接声明在:


    <style name="AppTheme" parent="AppBaseTheme">
      "android:listDivider">@drawable/divider_bg  
    style>

可以个自己写个Drawable,示例:


<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <gradient
        android:centerColor="#ff00ff00"
        android:endColor="#ff0000ff"
        android:startColor="#ffff0000"
        android:type="linear" />
    <size android:height="4dp"/>

shape>

1.2 ItemDecoration解析

  • 从上面的简单示例可知,要使用ItemDecoration,必须的先自定义,继承ItemDecoration。
public class MyDecoration extends RecyclerView.ItemDecoration {
}
  • 要实现自定义的装饰效果,必须重写getItemOffsets()onDraw()
public class MyDecorationOne extends RecyclerView.ItemDecoration {

    /**
     * 画线
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    /**
     * 设置条目周边的偏移量
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);

    }
}

在详细介绍这两个方法前,先解释ItemDecoration的含义。ItemDecoration是对Item起装饰作用
Android控件——RecyclerView系列三(Item Decoration)_第1张图片
getItemOffsets()就是设置item周边的偏移量(也就是装饰区域的宽度),而onDraw()才是真正实现装饰的回调方法,该方法可以在装饰区域任意画画。
Android控件——RecyclerView系列三(Item Decoration)_第2张图片
如上图,绿色区域代表内容,红色区域代表我们自己绘制的装饰,可以看到:

图1:代表了getItemOffsets(),可以实现类似padding的效果
图2:代表了onDraw(),可以实现类似绘制背景的效果,内容在上面
图3:代表了onDrawOver(),可以绘制在内容的上面,覆盖内容

假设我们要实现线性列表的分割线(LinearLayoutManager)

1)当线性列表是水平方向时,分割线竖直的;当线性列表是竖直方向时,分割线是水平的。

2)当画竖直分割线时,需要在item的右边偏移出一条线的宽度;当画水平分割线时,需要在item的下边偏移出一条线的高度

/**
 * 画线
 */
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
    if (orientation == RecyclerView.HORIZONTAL) {
        drawVertical(c, parent, state);
    } else if (orientation == RecyclerView.VERTICAL) {
        drawHorizontal(c, parent, state);
    }
}


/**
* 设置条目周边的偏移量
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    if (orientation == RecyclerView.HORIZONTAL) {
        //画垂直线
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    } else if (orientation == RecyclerView.VERTICAL) {
        //画水平线
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    }
}

因为getItemOffsets()是相对每个item而言的,即每个item都会偏移出相同的装饰区域。而onDraw()则不同,它是相对Canvas来说的,通俗的说就是要自己找到要画的线的位置

/**
 * 在构造方法中加载系统自带的分割线(就是ListView用的那个分割线)
 */
public MyDecorationOne(Context context, int orientation) {
    this.orientation = orientation;
    int[] attrs = new int[]{android.R.attr.listDivider};
    TypedArray a = context.obtainStyledAttributes(attrs);
    mDivider = a.getDrawable(0);
    a.recycle();
}

/**
 * 画竖直分割线
 */
private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int left = child.getRight() + params.rightMargin;
        int top = child.getTop() - params.topMargin;
        int right = left + mDivider.getIntrinsicWidth();
        int bottom = child.getBottom() + params.bottomMargin;
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

/**
 * 画水平分割线
 */
private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int left = child.getLeft() - params.leftMargin;
        int top = child.getBottom() + params.bottomMargin;
        int right = child.getRight() + params.rightMargin;
        int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

Android控件——RecyclerView系列三(Item Decoration)_第3张图片


如果想要使用ItemDecoration绘制表格,与画分割线步骤是类似的:画条分割线只需要2步,1是在item的下方偏移出一定的宽度,2是在偏移出来的位置上画线。画表格线其实也一样,除了画item下方的线,还画item右边的线就好了(当然换成左边也行)。

表格绘制示例

这里使用的布局管理器为网格列表(GridLayoutManager

  • 自定义分割线
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

    <solid android:color="#f00"/>
    <size
        android:width="2dp"
        android:height="2dp"/>

</shape>
  • 自定义ItemDecoration
public class MyDecorationTwo extends RecyclerView.ItemDecoration {

    private final Drawable mDivider;

    public MyDecorationTwo(Context context) {
        mDivider = context.getResources().getDrawable(R.drawable.divider);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        drawVertical(c, parent);
        drawHorizontal(c, parent);
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int top = child.getTop() - params.topMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int bottom = child.getBottom() + params.bottomMargin;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawHorizontal(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int top = child.getBottom() + params.bottomMargin;
            int right = child.getRight() + params.rightMargin;
            int bottom = top + mDivider.getMinimumHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

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

Android控件——RecyclerView系列三(Item Decoration)_第4张图片
表格的最后一列和最后一行不应该出现边边,那就让最后一列和最后一行的边边消失就好了。有以下几个思路。

  • onDraw()方法中,判断当前列是否为最后一列和判断当前行是否为最后一行来决定是否绘制边边。
  • getItemOffsets()方法中对行列进行判断,来决定是否设置条目偏移量(当偏移量为0时,自然就看不出边边了)。

getItemOffsets()有两个,一个是getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state),另一个是getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent),第二个已经过时,但是该方法中有回传当前item的position,所以我选用了过时的getItemOffsets()。

@Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
    super.getItemOffsets(outRect, itemPosition, parent);
    int right = mDivider.getIntrinsicWidth();
    int bottom = mDivider.getIntrinsicHeight();

    if (isLastSpan(itemPosition, parent)) {
        right = 0;
    }

    if (isLastRow(itemPosition, parent)) {
        bottom = 0;
    }
    outRect.set(0, 0, right, bottom);
}

public boolean isLastRow(int itemPosition, RecyclerView parent) {
    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager) {
        int spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
        int itemCount = parent.getAdapter().getItemCount();
        if ((itemCount - itemPosition - 1) < spanCount)
            return true;
    }
    return false;
}

public boolean isLastSpan(int itemPosition, RecyclerView parent) {
    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    if (layoutManager instanceof GridLayoutManager) {
        int spanCount = ((GridLayoutManager) layoutManager).getSpanCount();
        if ((itemPosition + 1) % spanCount == 0)
            return true;
    }
    return false;
}

1.2.1 onDrawOver

重写该方法,可以绘制在内容上面。例如现在很多电商App都会给商品加上一个标签,比如:推荐、热卖、秒杀等等。

简单示例:

public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration {
    private int tagWidth;
    private Paint leftPaint;
    private Paint rightPaint;

    public LeftAndRightTagDecoration(Context context) {
        leftPaint = new Paint();
        leftPaint.setColor(context.getResources().getColor(R.color.colorAccent));
        rightPaint = new Paint();
        rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary));
        tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width);
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            int pos = parent.getChildAdapterPosition(child);
            boolean isLeft = pos % 2 == 0;
            if (isLeft) {
                float left = child.getLeft();
                float right = left + tagWidth;
                float top = child.getTop();
                float bottom = child.getBottom();
                c.drawRect(left, top, right, bottom, leftPaint);
            } else {
                float right = child.getRight();
                float left = right - tagWidth;
                float top = child.getTop();
                float bottom = child.getBottom();
                c.drawRect(left, top, right, bottom, leftPaint);
            }
        }
    }

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

其中,ItemDecoration是可以叠加使用的,不仅仅只能使用一个。

Item Decoration深入浅出

你可能感兴趣的:(Android视图动效,Android基础)