1. 程式人生 > >LowRecyclerView系列之:RecyclerView之新增分割線

LowRecyclerView系列之:RecyclerView之新增分割線

關於Recyclerview新增分割線的部落格很多,但是很多部落格關於分割線都是講怎麼實現的,沒有講原理,而鴻洋大神的關於分割線的文章介紹的很詳細,但是感覺有些地方仍然不是太清楚。這篇文章以初學者的角度向大家詳細介紹了分割線新增的原理,詳細的解讀原始碼,力求讓大家能夠輕鬆理解dividers如何新增。同時解析LowRecyclerView的通用分割線的實現。

原始碼分析

使用過RecyclerView的小夥伴們對recyclerView.addItemDecoration()這個方法很熟悉,沒錯我們都是通過這個方法來新增分割線的。我們翻閱RecyclerView可以瞭解他,原始碼如下:

    /**
     * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
     * affect both measurement and drawing of individual item views.
     *
     * <p>Item decorations are ordered. Decorations placed earlier in the list will
     * be run/queried/drawn first for their effects on item views. Padding added to views
     * will be nested; a padding added by an earlier decoration will mean further
     * item decorations in the list will be asked to draw/pad within the previous decoration's
     * given area.</p>
     *
     * @param
decor Decoration to add */
public void addItemDecoration(ItemDecoration decor) { }

第一段註釋的大致意思是說addItemDecoration()這個方法主要是給RecyclerView新增一個ItemDecoration(中文譯作Item裝飾,我們常說的分割線就是用來裝飾我們的Item),Item decorations 他可以影響單個Item檢視的measurement(度量)和drawing(繪圖),仔細分析這句話的意思就是說我們的分割線是通過改變Item來繪製一定尺寸的分割線。

第二段主要是說:Item Decorations是有序的。在list將要被執行/查詢/繪製之前,Decoration會首先改變list的item檢視。Decoration將會以巢狀的形式填充到item檢視中去;在list被繪製的時候先去將Decoration的空間偏移出來。

通過上面兩段我們大致知道Decoration是通過在item裡面另外開闢的空間進行繪製的,那麼我們怎麼才能對分割線的空間大小進行控制呢,在addItemDecoration()方法中有一個ItemDecoration引數,我們接檢視他的原始碼,如下:

 /**
     * An ItemDecoration allows the application to add a special drawing and layout offset
     * to specific item views from the adapter's data set. This can be useful for drawing dividers
     * between items, highlights, visual grouping boundaries and more.
     *
     * <p>All ItemDecorations are drawn in the order they were added, before the item
     * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
     * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
     * RecyclerView.State)}.</p>
     */
    public static abstract class ItemDecoration 

一個ItemDecoration允許一個應用從adapter的adta的集合中添一個特殊的drawing和佈局偏移到具體的item檢視,他能夠被用於給item,highlights,視覺化分組邊界等繪製dividers(分割線),翻譯的有點不通順,簡單的說ItemDecoration可以用於繪製item的dividers。
接下來我們來看一下ItemDecoration中的方法,如下:
這裡寫圖片描述
可以看到共有6個方法其中三個過時的方法,我們不再看,剩下三個方法,我們先來看getItemOffsets()方法:

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

第一段的意思是:這個方法是用來檢索得到的Item的偏移量,outRect是用來指定Item檢視的偏移或者margin的畫素,這個方法預設實現是outRect將偏移量或者margin設定為0。
第二段的意思是說:如果想ItemDecoration不影響Item檢視的位置和大小,應該在outRect方法返回之前設定他的上、下、左、右四個引數的偏移量。
第三段的意思是說如果你想得到當前view在adapter裡的data集合的position,請用recyclerView的例項呼叫getChildAdapterPosition(View view)方法。
通過這三段註釋我們已經能夠清晰地瞭解它的使用了吧,哈哈,不得不說研究原始碼真的很有用,不說廢話繼續看另一個方法onDraw,原始碼如下:

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

這個方法將分割線繪製到recyclerView的畫布上,這個方法會將分割線在item繪製之前優先繪製出來,分割線會被繪製到item的下面。
我們再來看最後一個方法:

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

我們可以看到這個方法跟onDraw方法解釋大致相同,唯一的區別就是onDraw是優先於Item繪製,而onDrawOver是在item繪製之後進行繪製的,onDraw繪製的裝飾物是在item之下而onDrawOver是在item之上繪製的。可能對於對View研究很透徹的大牛來說,很容易理解,對於剛接觸這方面的小夥伴們來說就非常的困難,下面將詳細說名這兩個方法的區別。
首先來說再Item之前繪製,我們會優先繪製分割線,由於Item的高度和寬度是固定的而我們的分割線是在item的基礎上進行偏移的,我們可以在偏移的位置先將分割線繪製出來,然後再繪製Item,而onDrawOver是先繪製Item的,如果我們先繪製出了item那麼item的高度就固定了,我們在item的上面繪製分割線的時候,分割線就會在item之上進行繪製,繪製的分割線就會把我們的item給覆蓋,這樣帶來的體驗非常不好。

如何新增分割線

我們通過對ItemDecoration詳細的分析,大家應該能夠理解新增分割線的原理了吧,簡單的概述起來就是通過getItemOffsets()方法在讓Item進行偏移或者新增margin,然後呼叫onDraw在我偏移的地方繪製我們的裝飾物。
我們以GridView新增分割線為例,首先建立一個類去繼承ItemDecoration,在構造方法中傳入分割線的顏色,寬度和gridview的列數,如:

    /**
     * 自定義分割線(高和顏色)
     * @param dividerHeight 分割線高度
     * @param dividerColor  分割線顏色
     * @param nunColumns RecyclerView的行數或列數
     */
    public CommonRecyclerDivider(int dividerHeight, int dividerColor, int nunColumns) {
        mDividerHeight = dividerHeight;
        mNunColumns = nunColumns;
        mAdapter = adapter;
//        初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(dividerColor);
        //填充
        mPaint.setStyle(Paint.Style.FILL);
    }

在該方法中我們先初始化畫筆的顏色,然後從寫getItemOffsets()方法,分別將item底部和右側添進行偏移,然後判斷最後一列右側不偏移,程式碼如下:

 /**
     * 設定分割線
     * 先呼叫此方法(設定完後在執行onDraw方法)
     */
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    //獲取每個item檢視的posiotion
        int itemPosition = parent.getChildAdapterPosition(view);
     if(itemPosition%mNunColumns==mNunColumns-1){
        outRect.set(0, 0, 0, mDividerHeight);
        }else{
        outRect.set(0, 0, mDividerHeight, mDividerHeight);
        }
    }

然後我們在onDraw方法裡進行分割線的繪製,程式碼如下:

/**
     * 繪製分割線
     * getItemOffsets方法呼叫後會呼叫此方法
     */
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        //繪製底部
        drawButtom(c, parent);
        //繪製右側
        drawRigth(c, parent);
    }
    /**
     * 畫divider (底部)
     * @param c
     * @param parent
     */
    private void drawButtom(Canvas c, RecyclerView parent) {
        //獲取Item的數量
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            //獲取對應poisition對應的item
            final View child = parent.getChildAt(i);
            int left = child.getLeft();
            int right = child.getRight() + mDividerHeight;
            int top = child.getBottom();
            int bottom = top + mDividerHeight;
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }
/**
     * 畫divider(右邊)
     *
     * @param c
     * @param parent
     */
    private void drawRigth(Canvas c, RecyclerView parent) {
        //獲取Item的數量
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            //獲取對應poisition對應的item
            final View child = parent.getChildAt(i);
            int top = child.getTop();
            int left = child.getRight();
            int bottom = child.getBottom();
            int right = left + mDividerHeight;
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }

如果上面的程式碼你看不太懂就接和下面的這幅圖去理解:
這裡寫圖片描述
通過這張圖我想你應該能夠理解分割線的繪製了。其他的不管是listview還是瀑布流繪製方法和gridView繪製方法一樣,但是由於瀑布流的高度是變化的,所以無法判斷哪個Item是最後一列,所以瀑布流新增分割線的時候最後一列的右側是繪製出來的。

封裝通用的分割線

關於通用的分割線原理基本上跟上面講述的原理基本相同,我們需要根據recyclerView的顯示型別進行判斷即可,小夥伴們可以參照上述方法進行封裝,也可以使用我的開源專案LowRecyclerView中的CommonRecyclerDivider,只需要簡單的修改即可使用。如果文章有錯誤,歡迎大家指正!!!