Android RecyclerView-使用Itemdecoration實現粘性頭部功能,詳細到具體步驟.

一 前言

該文詳細的介紹了RecyclerView.ItemDecoration實現分組粘性頭部的功能,讓咱們本身生產代碼,告別代碼搬運工的時代.另外文末附有完整Demo的鏈接.看下效果:
json

二 知識準備

RecyclerView.ItemDecoration對於咱們最熟悉的功能就是給RecyclerView實現各類各樣自定義的分割線了,實現分割線的功能其實和實現粘性頭部的功能大同小異,那咱們就來看看這神奇的RecyclerView.ItemDecoration.bash

該類是RecyclerView的內部靜態抽象類:ide

public abstract static class ItemDecoration {
       /**
        * 繪製*除Item內容*之外的佈局,這個方法是再****Item的內容繪製以前****執行的,
        * 因此呢若是兩個繪製區域重疊的話,Item的繪製區域會覆蓋掉該方法繪製的區域.
        * 通常配合getItemOffsets來繪製分割線等.
        *
        * @param c      Canvas 畫布
        * @param parent RecyclerView
        * @param state  RecyclerView的狀態
        */
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }
        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        /**
         * 繪製*除Item內容*之外的東西,這個方法是在****Item的內容繪製以後****才執行的,
         * 因此該方法繪製的東西會將Item的內容覆蓋住,既顯示在Item之上.
         * 通常配合getItemOffsets來繪製分組的頭部等.
         *
         * @param c      Canvas 畫布
         * @param parent RecyclerView
         * @param state  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);
        }

        /**
         * 設置Item的佈局四周的間隔.
         *
        * @param outRect 肯定間隔 Left  Top Right Bottom 數值的矩形.
        * @param view    RecyclerView的ChildView也就是每一個Item的的佈局.
        * @param parent  RecyclerView自己.
        * @param state   RecyclerView的各類狀態.
         */
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

複製代碼

這裏面呢有個問題必定要明白幾個問題:佈局

  • getItemOffsets這個方法設置的Item間隔究竟是那個間隔?測試

    咱們來看一張圖. ui

咱們知道getItemOffsets()第一個參數是一個矩形的對象,這個對象的left、 top、right、bottpm四個屬性值分別表示圖中的outRect.left、outRect.top、outRect.right、outRect.bottom四個線段所表示的空間.也就是說當RecyclerView的Item再肯定本身的大小的時候會將getItemOffsets()裏面的Rect對象的Left、Top、Right、Bottom屬性取出來,看看須要再Item佈局的四周留出多大的空間.咱們來看下源碼:this

Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
        //這裏呢mTempRect就是咱們再getItemOffsets()裏面的第一個Rect的對象,咱們再實現類的方法裏面給mTempRect賦值.
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }
    
    
    這裏呢就是RecyclerView再測量每一個Child的大小的時候都把insets這個矩形的l t r  b 數值都加上了.insets就是方法getItemDecorInsetsForChild()返回的矩形對象.
     /**
         * Measure a child view using standard measurement policy, taking the padding
         * of the parent RecyclerView and any added item decorations into account.
         *
         * <p>If the RecyclerView can be scrolled in either dimension the caller may
         * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
         *
         * @param child Child view to measure
         * @param widthUsed Width in pixels currently consumed by other views, if relevant
         * @param heightUsed Height in pixels currently consumed by other views, if relevant
         */
        public void measureChild(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

複製代碼

源碼的講解過於粗糙,但願你們見諒,目的就是爲了讓你們知道這個getItemOffsets()方法是怎麼讓RecyclerView再Item以外留出空間的.spa

  • onDraw()和onDrawOver()方法應該用哪個?.net

    首先咱們看過上面的代碼以後知道,onDraw執行再Item的繪製以前,也就是ItemDecoration的onDraw方法先執行,再執行Item的onDraw方法,這樣Item的內容就會覆蓋在ItemDecoration的onDraw上面.ItemDecoration的onDrawOver()方法執行在Item的繪製以後,那就是onDrawOver()繪製的內容會覆蓋再Item內容之上.這樣就造成了層層遮蓋的問題,那麼咱們日常的分割線一般繪製在ItemDecoration的onDraw()方法裏面,爲了不Item的內容覆蓋掉,咱們就要getItemOffsets()爲咱們留出繪製的空間了.這樣咱們的思路不是不有了呢.3d

    咱們能夠用onDrawOver()和getItemOffsets()方法一塊兒使用來實現Item的粘性頭部和頂部懸浮的效果.

    三 代碼部分

    需求分析:這部分實際上是寫代碼前尤其重要的一部分,再分析的過程當中你能夠知道咱們要完成的是哪些功能,用什麼東西去完成,怎麼才能更好的去完成.最後本身能肯定出一套完美實現需求的方案.

    咱們要作的是區域分組顯示,每一個分組的開始要有一個粘性頭部.如圖所示:

  • 數據準備

首前後臺返回的數據必定要有組類區分,每一個分組的標記不能同樣,最好是咱們方便處理的.該Demo採用的標記位是int類型的標記tag,每組的標記以此+1,每五個城市分爲一組,每組的第一個城市當作頭部局顯示的內容.咱們的分組頭部的高度爲40dp.

  • getItemOffsets()
    該方法再recyclerView的每一個Item測量大小的時候都會被調用到, 咱們要在該方法裏面判斷出那個HeadItem而且給HeadItem留出繪製的空間,這裏有兩種方式.
    第一種方式:
    給Item 的Top留出空間,也就是outRect.top屬性賦值.
    第二種方式:
    給Item 的Bottom留出空間也就是outRect.bottpm屬性賦值.
    由於咱們在列表一開始的時候就要繪製一次Head,也就是說咱們要留出Head的空間,那麼咱們只能選擇第一種方法去預留空間了. 當你選擇方式1的時候,給outRect.top賦值,這樣的話咱們判斷是不是HeadItem的話就要拿當前Item的標記跟前一個Item的標記判斷了.若是用第二種的話就要用當前的標記跟下一個Item的標記判斷了.
    下面我來解釋下第一種方式,第二種方式雷同:
    a b c d e f g h i
    分組1 abc
    分組2 def
    分組3 ghi
    若是 a d g 是HeadItem . a的tag = 1 , b的tag = 1, c 的tag = 1....d的tag = 2,e的tag = 2 ,f的tag = 2,g的tag = 3...等等 .
    前一個Item的tag用 preTag 來表示 ,初始值爲 -1.
    假如當前的Item爲a,當前tag = 1,那麼它的前一個Item爲空,也就是發現preTag和a的tag不同,那麼a就是分組的頭部.
    假如當前的Item爲b,當前tag = 1,那麼它前一個preTag 也就是a的tag = 1,發現同樣那就是是同一組的.
    假如當前的Item爲d,當前tag = 2,那麼它前一個preTag 也就是c的tag = 1,發現前一個的tag跟當前的不同,那麼當前的就是新分組的第一個頭部Item.代碼是最有說服力的,下面來看代碼:
@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        if (citiList == null || citiList.size() == 0) {
            return;
        }
        int adapterPosition = parent.getChildAdapterPosition(view);
        RecBean.CitiListBean beanByPosition = getBeanByPosition(adapterPosition);
        if(beanByPosition == null){
            return;
        }
        int preTage = -1;
        int tage = beanByPosition.getTage();
        //必定要記住這個 >= 0
        if(adapterPosition - 1 >= 0) {
            RecBean.CitiListBean nextBean = getBeanByPosition(adapterPosition - 1);
            if (nextBean == null) {
                return;
            }
            preTage = nextBean.getTage();
        }
        if(preTage != tage){
            outRect.top = headHeight;
        }else {
            //這個目的是留出分割線
            outRect.top = lineHeight;
        }

    }

複製代碼

這樣下來咱們給分組頭部的空間就預留出來了.接下來繪製分組頭部,由於分割線我直接顯示的背景色因此就不用去繪製分割線了.

  • onDrawOver()
    這個方法裏面咱們要作的不僅是繪製Head,當列表滑動的時候RecyclerView會不斷的加載以後的Item,佈局發生複用,咱們要在不斷的變化中去從新繪製咱們的HeadItem的佈局.這個方法當每一個Item消失或者出現的時候都會被調用,咱們在這裏去繪製HeadItem的區域.因此在該方法裏面咱們會遍歷全部可見的Item去從新判斷那個Item有Head,而後去繪製.
    1.判斷頭佈局繪製頭佈局 ?
    那麼咱們在這裏呢仍是須要判從新去判斷哪一個Item是有Head.按照getItemOffsets裏面的咱們須要跟以前的Item的tag作比較.可是有個問題就是咱們再這裏並不能拿到Item的佈局或者別的東西,只能遍歷全部已經顯示的Item,也就是隻能一個個的將RecyclerView的ChildView拿出來.這樣的話咱們的前一個preTag就須要咱們本身去定義,而後用preTag來記錄咱們遍歷過的ChildView的Tag,當遍歷到下個Item的Tag跟以前的preTag同樣的話,那就繼續遍歷不去繪製頭佈局,當遍歷到Item的tag跟preTag不同的時候就去繪製有佈局.由於滑動的Item都會做爲RecyclerView的第0個ChildView出現,咱們拿不到它以前的Item的tag.
    2.怎麼讓頭佈局懸停在頂部 ?
    這個問題其實拿一個場景去說明是最好的了.當咱們HeadItem正好出如今屏幕的頂部的時候,咱們繼續滑動列表HeadItem就會漸漸的消失,也就是Item的getTop距離會小於咱們HeadItem的Head的高度,當出現這種狀況的時候, 咱們就讓Item的getTop和Head的高度中去選擇一個最大值.這樣就好保證當HeadItem畫出屏幕的時候Head佈局一直留在頂部.
    3.下個頭部來的時候怎麼替換呢 ?
    當頂部有一個頭部局在懸停的時候,咱們滑動列表時下個頭部確定會和當前懸停的頭部相遇.咱們再這裏作的是當前懸浮的頭佈局跟下個頭佈局相遇發生交替的時候有個漸變的效果.由於該方法在每個Item出現或者消失的時候都會執行,每當執行的時候都要遍歷一遍當前已經顯示的Item佈局,那麼必定會出現,當前遍歷的第0個Item正好是屏幕中的第一個Item,它的下一個Item正好是分組的頭布.這樣的話再往前滑的時候就會出現頭部交替的狀況.咱們這裏就須要判斷下一個Item是否是有頭佈局的Item,比較的方法就是用當前Item數據的tag跟下一個Item數據的nextTag比較,若是不一樣的話那下個Item就是有頭佈局的.若是同樣的話就continue繼續遍歷.再列表滑動的時候回一直繪製全部可見Item的Head佈局.
    4.漸變效果呢?
    上面咱們知道了當下個HeadItem跟屏幕頂部的Head相遇的時候就要發生交替.交替的時候有個漸變效果,也就是以前再屏幕頂部懸停的Head要隨着一個Item的消失而消失.下個Head要滑動到屏幕以後停在那裏.那就好辦了當onDrawOver()方法執行的時候,RecyclerView的第0個ChildView正好是屏幕頂部的Item,當它的下一個Item有個Head的時候,咱們只須要將當前Item的getTop數值賦值給繪製Head的矩形的bottpm屬性就能夠了.咱們必定要明白當出現Item出現消失的時候Head是再不斷的繪製的.

上代碼:

@Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if(citiList == null || citiList.size() == 0){
            return;
        }

        int parentLeft = parent.getPaddingLeft();
        int parentRight = parent.getWidth() - parent.getPaddingRight();

        int childCount = parent.getChildCount();
        int tag = -1;
        int preTag;
        for (int i = 0; i <childCount; i++) {
            View childView = parent.getChildAt(i);
            if(childView == null){
                continue;
            }
            int adapterPosition = parent.getChildAdapterPosition(childView);
            當前Item的Top
            int top = childView.getTop();
            int bottom = childView.getBottom();
            preTag = tag;
            tag = citiList.get(adapterPosition).getTage();
            //判斷下一個是否是分組的頭部
            if(preTag == tag){
                continue;
            }
            //這裏面我把每一個分組的頭部顯示的文字列表單獨提出來了,爲了測試方便用,
            String name = index.get((tag - 1 ) < 0 ? 0 : (tag -1));
            int height = Math.max(top,headHeight);
            //判斷下一個Item是不是分組的頭部
            if(adapterPosition + 1 < citiList.size()){
                int nextTag = citiList.get(adapterPosition + 1).getTage();
                if(tag != nextTag){
                   //這裏就是實現漸變效果的地方
                   //由於若是遍歷到
                    height = bottom;
                }
            }
            paint.setColor(Color.parseColor("#ffffff"));
            c.drawRect(parentLeft,height - headHeight,parentRight,height,paint);
            paint.setColor(Color.BLACK);
            paint.getTextBounds(name, 0, name.length(), rectOver);

            c.drawText(name, dip2px(10), height - (headHeight - rectOver.height()) / 2, paint);

        }


    }

複製代碼

到這裏咱們的功能已經結束了,咱們要知道getItemOffsets()會提早執行,每一個Item的回收和出現都會執行一次.onDraw或者onDrawOver再屏幕中的Item發生變化的時候都會執行,只要發生變化.咱們的Head會不停的繪製.

結束

這是2018年的第一篇文章,以前太忙了也沒好好的總結知識點.寫的倉促但願你們多多指導文章出現的問題,謝謝你們的反饋,歡迎評論吐槽哦~

歡迎你們關注
個人掘金
個人CSDN
個人簡書

Demo下載

喜歡文字的同窗也能夠關注該公衆號,與你一塊兒同遊文字的海洋~

相關文章
相關標籤/搜索