Android RecyclerView使用详解三

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_30276961/article/details/50241289

上一篇我介绍了水平线性布局和RecyclerView动态添加删除数据行时的动画效果。这一篇将讲下RecyclerView的分隔图自定义。

对于RecyclerView,它的分隔图没有ListView来的那么简单,只要设置android:divider属性就行。它把分隔图的绘制给抽离出来了。这样做牺牲了设置的简单性,但是也带来了灵活性。

比方说,ListView的分隔图,我不能简单做到隔一行显示或者其他逻辑方式处理。不过在RecyclerView,就可以很轻易的实现。因为分隔图的绘制范围是受你控制,你爱咋画就咋画。

ok,说了这么多,先来看看添加分隔图的效果
这里写图片描述 这里写图片描述

上面包含LinearLayout和GridLayout的横向和纵向的分隔图,至于瀑布流的添加,其实和Grid的分隔图差不多。为了看起来清晰一点,我把分隔的高度调大了。

那么,要实现自定义分隔图,首先,我们得熟悉一下它的接口。

    public static abstract class ItemDecoration {
        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn before the item views are drawn,
         * and will thus appear underneath the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView
         */
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        /**
         * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
         * Any content drawn by this method will be drawn after the item views are drawn
         * and will thus appear over the views.
         *
         * @param c Canvas to draw into
         * @param parent RecyclerView this ItemDecoration is drawing into
         * @param state The current state of RecyclerView.
         */
        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }

        /**
         * @deprecated
         * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
         */
        @Deprecated
        public void onDrawOver(Canvas c, RecyclerView parent) {
        }


        /**
         * @deprecated
         * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
         */
        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        /**
         * Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
         * the number of pixels that the item view should be inset by, similar to padding or margin.
         * The default implementation sets the bounds of outRect to 0 and returns.
         *
         * <p>
         * If this ItemDecoration does not affect the positioning of item views, it should set
         * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
         * before returning.
         *
         * <p>
         * If you need to access Adapter for additional data, you can call
         * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
         * View.
         *
         * @param outRect Rect to receive the output.
         * @param view    The child view to decorate
         * @param parent  RecyclerView this ItemDecoration is decorating
         * @param state   The current state of RecyclerView.
         */
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

上面的抽象类就是我们需要复写的对象,可以看到它包含了如下接口:

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)

过时的就不列出来了。
这里可以看到有两个draw,这两者的区别正如它们的名称,一个在前画,一个在后画。
onDraw在每个item画之前先开始画,可以被item的内容覆盖。
onDrawOver在每个item画完之后再开始画,可以覆盖item的内容。

一般来说,我们可以选择任意一个复写就行。如果有特殊需求,比方说要透明覆盖在item上面,这样的话,就可以用onDrawOver。

getItemOffsets是设置每个item的偏移量,这个偏移量部分一般就是用来绘制分隔图。

好了,了解了这些接口,我就直接贴出LinearLayoutItemDecoration代码:

扫描二维码关注公众号,回复: 3845084 查看本文章
public class LinearLayoutItemDecoration extends RecyclerView.ItemDecoration{
    final Context mContext;
    final int mOrientation;
    final Drawable mDividerDrawable;
    int mDividerHeight;

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

    public LinearLayoutItemDecoration(Context context, int orientation) {
        mContext = context;
        mOrientation = orientation;

        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        mDividerHeight = ta.getDimensionPixelSize(1, 1);
        ta.recycle();
    }

    public LinearLayoutItemDecoration(Context context, int orientation, int dividerHeight) {
        mOrientation = orientation;
        mContext = context;
        mDividerHeight = dividerHeight;
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        ta.recycle();
    }

    public LinearLayoutItemDecoration(Context context, int orientation, Drawable dividerDrawable, int dividerHeight) {
        mContext = context;
        mOrientation = orientation;
        mDividerDrawable = dividerDrawable;
        mDividerHeight = dividerHeight;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
            drawHorizontal(c, parent);
        } else {
            drawVertical(c, parent);
        }
    }

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

        final int count = parent.getChildCount();
        for (int i = 0; i < count; 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+mDividerHeight;
            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth()-parent.getPaddingRight();

        final int count = parent.getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom()+params.bottomMargin;
            final int bottom = top + mDividerHeight;
            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == LinearLayoutManager.VERTICAL) {
            outRect.bottom = mDividerHeight;
        } else {
            outRect.right =  mDividerHeight;
        }
    }
}

我定义了一个id数组:

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

这样定义,是从主题里去获取资源属性。比方说,我可以定义如下主题:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
    </style>
    <style name="ItemDecorationTheme" parent="AppTheme">
        <!--<item name="android:listDivider">@drawable/img_line_x</item>-->
        <item name="android:listDivider">@drawable/line_divide</item>
        <item name="android:dividerHeight">1px</item>
    </style>

然后,就可以通过id数组和obtainStyledAttributes(ATTRS),获得主题里定义的属性。
其余代码就不讲了,需要注意的是parent.getChildCount()返回的是一屏的view数量,不是所有item的数量。

接着,看下Grid布局的分隔图,为了看起来清晰,我把垂直布局和横向布局分开写了。先来看看垂直布局:

public class GridLayoutItemDecoration extends RecyclerView.ItemDecoration{
    private static final String TAG = "GridLayoutItemDecoration";
    final Context mContext;
    final Drawable mDividerDrawable;
    int mDividerHeight;

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

    public GridLayoutItemDecoration(Context context) {
        mContext = context;
        // 从主题去获取属性键值
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        mDividerHeight = ta.getDimensionPixelSize(1, 1);
        ta.recycle();
    }

    public GridLayoutItemDecoration(Context context, int height) {
        mContext = context;
        // 从主题去获取属性键值
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        mDividerHeight = height;
        ta.recycle();
    }

    public GridLayoutItemDecoration(Context context, Drawable drawable) {
        mContext = context;
        mDividerDrawable = drawable;
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerHeight = ta.getDimensionPixelSize(1, 1);
        ta.recycle();
    }

    public GridLayoutItemDecoration(Context context, Drawable drawable, int height) {
        mContext = context;
        mDividerDrawable = drawable;
        mDividerHeight = height;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
        final int spanCount = manager.getSpanCount();
        drawHorizontal(c, parent, state, spanCount);
        drawVertical(c, parent, state, spanCount);
    }

    private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state, final int spanCount) {
        final int count = parent.getChildCount();
        // 确定有几行
        final int rowCount = count/spanCount + (count%spanCount==0?0:1);

        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        for (int i = 0; i < rowCount; i++) {
            final View child = parent.getChildAt(i*spanCount);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDividerHeight;

            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

    private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state, final int spanCount) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
        for (int i = 0; i < spanCount; 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 + mDividerHeight;
            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

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

        final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
        final int spanCount = manager.getSpanCount();
        final int position = parent.getChildAdapterPosition(view);
        final int count1 = parent.getChildCount();
        final int count = parent.getAdapter().getItemCount();
        Log.d(TAG, "getItemOffsets count = " + count1 + ", position = " + position);
        if ((position == count -1) && (position % spanCount) == (spanCount - 1)) {
            // 最后一个,如果也是最右边,那么就不需要偏移
        } else if (position >= (count - (count % spanCount))) {
            // 最下面一行,只要右边偏移就行
            outRect.right = mDividerHeight;
        } else if ((position % spanCount) == (spanCount - 1)) {
            // 最右边一列,只要下面偏移就行
            outRect.bottom = mDividerHeight;
        } else {
            // 其他的话,右边和下面都要偏移
            outRect.set(0, 0, mDividerHeight, mDividerHeight);
        }
    }
}

横竖分隔图的绘制看代码,很清晰,我直接是绘制在每个item的下面和右边。这里主要说下偏移值的设置。
根据grid的布局,可以分成四个情况:
1. 最后一个item,如果也是最右边那列里的,是不需要偏移值的
2. 最下面一行,不需要下面的偏移值
3. 最右边一列,不需要右边的偏移值。(因为最右边不需要画分隔图)
4. 其余情况,下面和右边都偏移就行。

需要注意的是,别用parent.getChildCount()去做为总数。应该用parent.getAdapter().getItemCount()。

横向grid的代码和纵向的类似,我贴下:

public class HorizontalGridLayoutItemDecoration extends RecyclerView.ItemDecoration{
    final Context mContext;
    final Drawable mDividerDrawable;
    int mDividerHeight;

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

    public HorizontalGridLayoutItemDecoration(Context context) {
        mContext = context;
        // 从主题去获取属性键值
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        mDividerHeight = ta.getDimensionPixelSize(1, 1);
        ta.recycle();
    }

    public HorizontalGridLayoutItemDecoration(Context context, int height) {
        mContext = context;
        // 从主题去获取属性键值
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerDrawable = ta.getDrawable(0);
        mDividerHeight = height;
        ta.recycle();
    }

    public HorizontalGridLayoutItemDecoration(Context context, Drawable drawable) {
        mContext = context;
        mDividerDrawable = drawable;
        TypedArray ta = context.obtainStyledAttributes(ATTRS);
        mDividerHeight = ta.getDimensionPixelSize(1, 1);
        ta.recycle();
    }

    public HorizontalGridLayoutItemDecoration(Context context, Drawable drawable, int height) {
        mContext = context;
        mDividerDrawable = drawable;
        mDividerHeight = height;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
        final int spanCount = manager.getSpanCount();
        drawHorizontal(c, parent, state, spanCount);
        drawVertical(c, parent, state, spanCount);
    }

    private void drawVertical(Canvas c, RecyclerView parent, RecyclerView.State state, int spanCount) {
        final int count = parent.getChildCount();
        // 确定有几列
        final int columnCount = count/spanCount + (count%spanCount==0?0:1);

        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        for (int i = 0; i < columnCount; i++) {
            final View child = parent.getChildAt(i*spanCount);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDividerHeight;
            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

    private void drawHorizontal(Canvas c, RecyclerView parent, RecyclerView.State state, int spanCount) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        for (int i = 0; i < spanCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDividerHeight;
            mDividerDrawable.setBounds(left, top, right, bottom);
            mDividerDrawable.draw(c);
        }
    }

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

        final GridLayoutManager manager = (GridLayoutManager) parent.getLayoutManager();
        final int spanCount = manager.getSpanCount();
        final int position = parent.getChildAdapterPosition(view);
        final int count = parent.getAdapter().getItemCount();

        if ((position == count -1) && (position % spanCount) == (spanCount - 1)) {
            // 最后一个,如果也是最下面一行,那么就不需要偏移
        } else if (position >= (count - (count % spanCount))) {
            // 最右边一列,只要下面偏移就行
            outRect.bottom = mDividerHeight;
        } else if ((position % spanCount) == (spanCount - 1)) {
            // 最下面一行,只要右边偏移就行
            outRect.right = mDividerHeight;
        } else {
            // 其他的话,右边和下面都要偏移
            outRect.set(0, 0, mDividerHeight, mDividerHeight);
        }
    }
}

代码不讲了,只需要注意一点,横向grid的item的索引是从上到下,从左到右,知道这点,就应该没问题了。

ok,基本的自定义分隔图讲完了,接下去,扩展一下思路,来体现一下自定义的好处。我就举个分隔图不断递宽的例子。
这里写图片描述

代码就不用贴了吧,其实就是在上述LinearLayoutItemDecoration的基础上,增加一个递增值就行。

接下去说说,怎么使用这些ItemDecoration。包含添加和移除:

RecyclerView.addItemDecoration(ItemDecoration);
RecyclerView.removeItemDecoration(ItemDecoration);

你可以给一个RecyclerView添加多个分隔图。

好了,关于RecyclerView的分隔图自定义讲解完了。

猜你喜欢

转载自blog.csdn.net/sinat_30276961/article/details/50241289