Aug 12

理解RecyclerView中ItemDecoration的使用

Lrdcq , 2016/08/12 19:24 , 程序 , 閱讀(4829) , Via 本站原創
ItemDecoration是我们在使用RecyclerView的时候常常会提到的,惟一的可插拔的样式性的拓展。我们一般经常会下载一些现成的ItemDecoration来完成一些特定的功能,比如官方提供的DividerItemDecoration来实现RecyclerView的分割线,第三方的PinnedItemDecoration来完成item吸顶效果。

深究

通过类名我们就可以知道,使用它可以为item进行样式上的装饰,这么宽泛的概念,我们到底怎么使用它,或者说它究竟本设计出来做什么的比较好呢?
  
1. 首先我们要理解,它不能改变item原有的东西,只能添加新的东西。比如前言中所说的分割线,就是强行在item下方绘制了新的东西。吸顶效果,在所有item上方绘制新的物体。

2. 本身与数据无关。RecyclerView与数据的唯一交互方式是Adapter,其它的组件设计之初分割的理由就是为了样式和数据分离。当然,在实际使用的过程中,可以通过getAdapter获取到,当前数据类型,数据长度等暴露出的信息。

3. 它完成的是绘制,不是布局。或者说,它只是向RecyclerView中直接画东西,而不是添加或者改变其中的布局。一方面,这是用可插拔性考虑的,多个ItemDecoration一起添加布局势必可能导致一些冲突或者错误。当然,直接往上面绘制,也有不方便的地方:一个是事件处理,不是界面就没法帮我们拦截事件,如果我们需要在添加的东西上进行事件处理的话,势必得通过其它方式对事件进行拦截并做相应的处理。同时如果不是界面,稍复杂的图文组合渲染也稍显困难(需要手动调用view的生命周期)。

了解到这么一些它的特性和弱点,当然,可插拔的组件当然会牺牲一些东西来实现特性,我们就可以仔细看看代码,清晰的看到它可以做什么了。

接口

回到代码层面,ItemDecoration本身很简单,就3个方法提供给我们进行实现。
public void onDraw(Canvas c, RecyclerView parent, State state);
public void onDrawOver(Canvas c, RecyclerView parent, State state);
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state);

我们从调用顺序开始讲起:

1. 首先调用的是getItemOffsets,它是在RecyclerView的测量阶段就被调用的。查看一下调用它的地方,是在RecyclerView的getItemDecorInsetsForChild方法调用的且上层被measureChild调用。稍微看看代码就很明显了,getItemOffsets可以提供一个矩形的inset来设定一个上下左右的间隔,并添加到每一个item上。如果觉得这一段文字说得很玄学,我们看看DividerItemDecoration里面怎么用的:
@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);
    }
}

如果是横向列表,在下方添加一个Divider的高度,否则,在右边添加一个Divider的宽度。嘛其实就是类似于item的margin一样拓展item嘛。实际代码中,我们可以看到确实就是和item的margin的数累加在了一起。所以清晰了。getItemOffsets是拓展每一个item的大小。

2. 然后调用的是onDraw。onDraw被调用在RecyclerView的onDraw一开始,当然在绘制子view之前,可以简单理解为,在RecyclerView的最下层随便画一些东西。

3. 而明显onDrawOver相反,它被调用在draw一开始,在绘制子view之后了,当然,就是在RecyclerView的顶层绘制一些东西,在它之后的绘制代码就只剩下针对RecyclerView的translate进行处理了。

可以注意到,这三个接口其实,只有getItemOffsets是针对单个item的,另外两个绘制接口是针对的整个RecyclerView进行绘制。这样可以方便的进行跨item的绘制。当然针对每个item进行绘制也是可以轻而易举的做到的,常见的方法是做这样一个循环:
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 + mVerticalSpaceHeight;
    mDrawable.setBounds(left, top, right, bottom);
    mDrawable.draw(c);
}

就可以简单的获得每一个item区域的上下左右,并将mDrawable绘制到对应的区域中。

实践

在之前开发过程中,针对viewHolder的可插拔特性封装出了一个叫holderBehavior的概念,深入了解ItemDecoration之后,才了解ItemDecoration完全可以做到holderBehavior设计之初想做的事儿。比如,我们来实现一些条纹状item背景的可插拔组件。

如果是不可插拔组件,和在holderBehavior处理,做这样的只需要根据position改变背景的颜色就可以了:
holder.itemView.setBackground(position % 2 == 0 ? mColorGray : mColorWhite);

而改写为ItemDecoration后,它是这样的:
public class StripedItemDecoration extends RecyclerView.ItemDecoration {
    public StripedItemDecoration(Context context) {
        this(context, R.color.colorWhite, R.color.colorGrey200);
    }

    public StripedItemDecoration(Context context, @DrawableRes int id1, @DrawableRes int id2) {
        mDivider = context.getResources().getDrawable(id1);
        mDivider2 = context.getResources().getDrawable(id2);
    }

    private Drawable mDivider, mDivider2;
    private List<Integer> type = new ArrayList<>();

    public StripedItemDecoration forType(int type) {
        this.type.add(type);
        return this;
    }


    public StripedItemDecoration forType(List<Integer> type) {
        this.type.addAll(type);
        return this;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        RecyclerView.Adapter adapter = parent.getAdapter();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final int position = parent.getChildAdapterPosition(child);
            if (position == RecyclerView.NO_POSITION) {
                continue;
            }
            final int viewType = adapter.getItemViewType(position);
            if (type.size() != 0 && !type.contains(viewType)) {
                continue;
            }
            final int top = child.getTop() + Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + child.getHeight();
            if (position % 2 == 1) {
                mDivider.setBounds(left, top, right, bottom);
                mDivider.draw(c);
            } else {
                mDivider2.setBounds(left, top, right, bottom);
                mDivider2.draw(c);
            }
        }
    }
}

考虑到实际的使用场景,由于本来只用针对某一种holder设置条纹变成了全局设置条纹了,所以通过type来限定了一下具体哪些item需要渲染。同时,两个drawable都可以自己设定,应该不但是普通的color,矢量图和图片都可以设置为条纹背景。最后是最核心的onDraw了。我们做了这么几个特别的事儿:

1. 限定使用LinearLayoutManager才能用条纹,因此通过LinearLayoutManager取到了当前第一个item的数据position。
2. 循环当前界面中的每一个item,且找到他在数据中的位置。
3. 同时,找到当前这个item的类型,通过设置的type甩掉不需要的类型
4. 计算位置,绘制条纹咯

可以看到,我们在所有界面下方绘制了需要的drawable,没有item区域拓展,也没做别的事儿,代码相当干净。不过有一个坑的地方在于,draw到画布上的drawable不是没有交互效果么,当然ripple效果也没了,ripple还是需要在item本体里面做。

再次深入

除了条纹效果,接下来计划做的还包括可选中的效果,也使用ItemDecoration给抽出来。也许会觉得很奇怪,ItemDecoration明明是装饰描述的,也可以用它来处理交互么?嗯,当然可以,参考兼容包中的官方类ItemTouchHelper的代码,如果说做做Divider是ItemDecoration的下限,随便做点什么东西都so easy,那它就是ItemDecoration的上限了,参考这个类,正在绝赞制作中!毕竟可插拔是一个非常诱人的特性呢,千万不要忽略。

本来大概的讲解就到这里并没有问题,但是深入阅读ItemTouchHelper的源码之后,我感觉前面有些错误。其中最重要的一点是,其实并不是不能修改原本item的界面绘制结果。事实上onDrawOver拿到的画布,正是已经完成绘制之后的结果,如果我们此时对画布进行切割,拆分,重组,就可以实现修改原本界面的目的。当然,写这样的ItemDecoration是有伤害的,再来一个工作模式类似的ItemDecoration组合使用就gg了,不过作为单一的解决方案,还是不错的。
logo