1. 程式人生 > >Android RecyclerView使用詳解三

Android RecyclerView使用詳解三

上一篇我介紹了水平線性佈局和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程式碼:

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的分隔圖自定義講解完了。