ItemDecoration深入解析與實(shí)戰(zhàn)(二)—— 實(shí)際運(yùn)用

一 概述

這是這個系列的第二篇文章欧芽,第一篇
ItemDecoration深入解析與實(shí)戰(zhàn)(一)——源碼分析 是偏原理性的涝涤,而這篇是偏應(yīng)用性的句占。沒看過上一篇文章對閱讀此文也基本沒多大影響哨免,不過了解原理會加深對本文Demo的理解茎活。

這篇文章將會實(shí)現(xiàn)上篇文章最后說的幾個實(shí)戰(zhàn)點(diǎn),包括:

  1. (LinearLayoutManager) 最簡單的分割線實(shí)現(xiàn)
  2. (LinearLayoutManager) 自定義分割線實(shí)現(xiàn)
  3. (GridLayoutManager) 網(wǎng)格布局下的均分等距間距(分割線)
  4. (StaggeredLayoutManger) 瀑布流布局下均分等距間距(分割線)
  5. (GridLayoutManager) 網(wǎng)格布局下實(shí)現(xiàn)表格式邊框
  6. 打造粘性頭部

看完這6點(diǎn)標(biāo)題琢唾,應(yīng)該會知道這篇文章的篇幅會稍長载荔,不過因?yàn)槭菍?shí)戰(zhàn)類型的文章,所以也不會特別枯燥采桃。

建議

1. 你需要具備怎樣的前提知識

  • 閱讀本文應(yīng)該有一定的 RecyclerView 使用基礎(chǔ)
  • 對 View 的基礎(chǔ)繪制使用有了解(沒有影響也不大)

2. 閱讀順序

  • 從頭到尾懒熙,這有個難易順序,讀下去會比較順暢
  • 由于文章較長普办,可以挑上面6點(diǎn)其中一個感興趣的進(jìn)行閱讀工扎,拉到下方每個點(diǎn)的第一部分都會有一個實(shí)現(xiàn)圖,可以觀看實(shí)際效果決定是否想要閱讀

二 實(shí)戰(zhàn)

1. (LinearLayoutManager) 最簡單的分割線實(shí)現(xiàn)

(1) 實(shí)現(xiàn)效果

image

(2) 具體實(shí)現(xiàn)

像這種單一顏色的分割線實(shí)現(xiàn)起來很簡單衔蹲,就是一行代碼:

public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                               @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        outRect.set(0,0,0,5);
    }
}

這個5對應(yīng)的就是outRect.bottom肢娘,看過這系列的上篇文章就能容易理解,這個跟在 ItemView的布局文件中增加一個 marginBottom是一樣的效果的舆驶。不過這樣默認(rèn)是沒有顏色的橱健,這個分割線的顏色就取決于 RecyclerView的背景顏色。如我們的效果圖的實(shí)現(xiàn):

image
RecyclerView rvTest = findViewById(R.id.rv_test);
rvTest.addItemDecoration(new SimpleDividerDecoration());

這種實(shí)現(xiàn)很簡單沙廉,但是缺點(diǎn)也很突出拘荡,因?yàn)樗且蕾囉?RecyclerView 的背景的,而如果我們?yōu)?RecyclerView 設(shè)置一個padding,就會變成這樣:

image

就是說萬一我們的需求是有padding,而且背景顏色要跟分割線顏色不同那就沒辦法了撬陵。如果要解決這一問題珊皿,就要看第2點(diǎn)网缝。

2. (LinearLayoutManager) 自定義分割線實(shí)現(xiàn)

(1) 實(shí)現(xiàn)效果

image

(2) 使用

由于 support 包中已經(jīng)有了一個默認(rèn)的實(shí)現(xiàn),所以就沒有自己寫了蟋定,這是官方自帶的 ItemDecoration實(shí)現(xiàn)類粉臊,先看下怎么用:

rvTest.setLayoutManager(new LinearLayoutManager(this));      
DividerItemDecoration decoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
decoration.setDrawable(getResources().getDrawable(R.drawable.divider_gradient));
rvTest.addItemDecoration(decoration);

在示例中,我為這個Decoration添加了一個 Drawable驶兜,這個 Drawable 就是上圖的一個分割線效果维费,如果沒有設(shè)置這個,那么將會有一個默認(rèn)的灰色分割線:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:endColor="#19f5e7"
        android:startColor="#b486e2" />
    <size android:height="4dp" />
</shape>

分割線的高度就是這個Drawable的高促王。

(3) 具體實(shí)現(xiàn)

用法很簡單犀盟,但正所謂知其然,還要知其所以然蝇狼,我們看一下這個 DividerItemDecoration 里面的具體實(shí)現(xiàn)是怎樣的:

  • 先看getItemOffsets方法的具體實(shí)現(xiàn)

// DividerItemDecoration.java

private Drawable mDivider;

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
        RecyclerView.State state) {
    if (mDivider == null) {
        outRect.set(0, 0, 0, 0);
        return;
    }
    if (mOrientation == VERTICAL) {
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());  //注釋1
    } else {
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

直接看注釋1阅畴,mOrientation == VERTICA的情況,在 getItemOffsets方法中迅耘,也是用了我們第1個實(shí)戰(zhàn)點(diǎn)中最簡單的那種方式贱枣,只不過他的高度變成了mDivider.getIntrinsicHeight()而已,這個mDivider就是我們 setDrawable中設(shè)置的一個 Drawable 對象颤专,如果沒有設(shè)置纽哥,那就會有一個默認(rèn)的。

  • 再看 onDraw方法

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getLayoutManager() == null || mDivider == null) {
        return;
    }
    if (mOrientation == VERTICAL) {
        drawVertical(c, parent);
    } else {
        drawHorizontal(c, parent);
    }
}

這里也分為兩種情況栖秕,我們直接看 VERTICAL 下的春塌,即 drawVertical(c, parent) 方法:

private void drawVertical(Canvas canvas, RecyclerView parent) {
    canvas.save();
    final int left;
    final int right;
    //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
    if (parent.getClipToPadding()) {
        left = parent.getPaddingLeft();
        right = parent.getWidth() - parent.getPaddingRight();
        canvas.clipRect(left, parent.getPaddingTop(), right,
                parent.getHeight() - parent.getPaddingBottom());
    } else {
        left = 0;
        right = parent.getWidth();
    }
    
    /***************分割***************/
    
    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);  //注釋1
        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}

我們先看注釋分割線的上邊,邏輯很簡單簇捍,主要就是為了拿到 Child 最大可用空間的左右邊界只壳,如果我們沒有設(shè)置,parent.getClipToPadding() 默認(rèn)是返回 ture 的暑塑,即最大可用空間的要減去RecyclerView的padding,這是為了讓padding不被分割線覆蓋吼句。

再看注釋分割線的下邊,這里遍歷了所有的 Child 事格。先看注釋1這句代碼惕艳,parent.getDecoratedBoundsWithMargins(child, mBounds),這個方法有什么用呢,其實(shí)看名稱就能大概猜出來,這個方法可以拿到

child邊界+decoration + margin

所組成的Rect的邊界值mbounds驹愚,即下圖里面的橙色區(qū)域的外邊框所對應(yīng)的值远搪。

image

注意:此圖不嚴(yán)謹(jǐn),詳細(xì)內(nèi)容請看這系列的上一篇文章

然后便會將mbounds的 bottom 跟 top ,以及 上面得到的 left 跟 right 設(shè)置到 mDivider的邊界中么鹤,就獲得的我們上圖的紅色虛線邊框的矩形终娃,如果我們沒有為 itemView 設(shè)置 margin味廊,那么就會得到綠色虛線邊框的范圍蒸甜,再將這部分畫出來棠耕,就得到了我們想要的分割線了。

3. (GridLayoutManager) 網(wǎng)格布局下的均分等距間距(分割線)

GridSpaceDecoration

(1) 實(shí)現(xiàn)效果

image

效果如上圖柠新,解決了下面的常見問題:

  1. 某些 item 占用多個 span 情況
  2. item 之前的間距相等
  3. item 的寬高可以保持一致窍荧,不會有某個 item 被壓扁的情況
  4. 上下左右的邊框可以與中間的分割線寬度不一致,每個都可以單獨(dú)設(shè)置

(2) 使用方法

public GridSpaceDecoration(int horizontal, int vertical){
    //...
}

public GridSpaceDecoration(int horizontal, int vertical, int left, int right){
    //...
}

/**
 * @param horizontal 內(nèi)部水平距離(px)
 * @param vertical   內(nèi)部豎直距離(px)
 * @param left       最左邊距離(px)恨憎,默認(rèn)為0
 * @param right      最右邊距離(px),默認(rèn)為0
 * @param top        最頂端距離(px),默認(rèn)為0
 * @param bottom     最底端距離(px),默認(rèn)為0
 */
public GridSpaceDecoration(int horizontal, int vertical, int left, int right, int top, int bottom){
    //...
}

該類提供了三個構(gòu)造方法蕊退,直接設(shè)置相應(yīng)的值,然后 add 到
RecyclerView中即可憔恳。

(3) 具體實(shí)現(xiàn)

step1: 分析

要實(shí)現(xiàn)的功能很清晰瓤荔,就是要解決上面的常見問題。其中钥组,第2输硝、3點(diǎn)比較麻煩,為什么呢?先分析一下

image

先看下上圖程梦,當(dāng)使用 GridLayoutManager 時点把,GridLayoutManager會將每個 Item 的最大可用空間平均分配開來,就像上圖黑線所對應(yīng)的三個框就是3個 Item 的最大可分配空間屿附。橙色區(qū)域就是 Decoration 設(shè)置的值跟 item 的 margin 郎逃,如果 margin 為0,那么橙色區(qū)域便是在 getItemOffsets 方法中設(shè)置的值(下面簡稱 offsets)挺份。綠色虛線所圍成的區(qū)域就是我們 itemView 的實(shí)際空間褒翰。

通過上圖,當(dāng)我們?yōu)?item 設(shè)置相同的間距時匀泊,會發(fā)現(xiàn) item 1 的空間被壓縮了影暴,那么怎么解決這一問題呢?

  1. 每個item 寬度相同
  2. item 之前的間距一樣

我們要解決的就是上面的問題

  • 先討論第1點(diǎn)探赫,因?yàn)槊總€ item 的最大可用空間(黑色框格子)是一致的型宙,所以想要讓 item 的寬度一樣,就是讓每個 item 的 offsets 保持一致伦吠。我們可以得到下面的公式:

    sizeAvg = (left + right + center * (spanCount-1)) / spanCount

    其中妆兑,left 、right 為最左毛仪、左右邊間距搁嗓,center 為中間間距,spanCount 為每一行的 span 個數(shù)箱靴,就可以得出每個 item 需要設(shè)置的 offsets 大小 sizeAvg腺逛,這樣就可以保證每個 item 的寬度一致(均分)

  • 再看第2點(diǎn),我們要保證每個中間間距都一樣衡怀,左右間距達(dá)到我們設(shè)置的大小棍矛。首先安疗,最左邊的間距是已經(jīng)確定了的,即 left,那么最左邊 item 的右邊 right1 就可以得出為 sizeAvg - left,第二個 item 左邊間距 left2 就是 center - right1 同理可以推出接下來的 item 够委,看下圖會更清晰:

    image

    然后把中間的實(shí)體線給去掉:

    image

    就可以看到每個 item 的寬度一樣了荐类,而且間距也是符合預(yù)期的效果。(圖片是人工畫的茁帽,可能會有點(diǎn)小誤差)

step2 實(shí)現(xiàn)

上面分析完成玉罐,接著看看算法實(shí)現(xiàn):

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                           @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    if (isFirst) {
        init(parent);
        isFirst = false;
    }
    if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
        handleVertical(outRect, view, parent, state);  //注釋1
    } else {
        handleHorizontal(outRect, view, parent, state);
    }
}

很簡單,先是做了一點(diǎn)初始化潘拨,然后分兩個方向進(jìn)行不同處理吊输。直接看注釋1(orientation == VERTICAL)部分:

private void handleVertical(Rect outRect, View view, RecyclerView parent,
                            RecyclerView.State state) {
    GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
    int childPos = parent.getChildAdapterPosition(view);
    int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
    int spanSize = lp.getSpanSize();
    int spanIndex = lp.getSpanIndex();
    outRect.left = computeLeft(spanIndex, sizeAvg);    //注釋1
    if (spanSize == 0 || spanSize == mSpanCount) {
        outRect.right = sizeAvg - outRect.left;
    } else {
        outRect.right = computeRight(spanIndex + spanSize - 1, sizeAvg);
    }
    outRect.top = mVertical / 2;
    outRect.bottom = mVertical / 2;
    if (isFirstRaw(childPos)) {
        outRect.top = mTop;
    }
    if (isLastRaw(childPos)) {
        outRect.bottom = mBottom;
    }
}

這里的 sizeAvg 就是我們上面分析的那個 sizeAvg,然后再調(diào)用 computeLeft 方法(注釋1)铁追,先看下這個方法這怎樣的實(shí)現(xiàn):

private int computeLeft(int spanIndex, int sizeAvg) {
    if (spanIndex == 0) {
        return mLeft;
    } else if (spanIndex >= mSpanCount / 2) {
        //從右邊算起
        return sizeAvg - computeRight(spanIndex, sizeAvg);
    } else {
        //從左邊算起
        return mHorizontal - computeRight(spanIndex - 1, sizeAvg);
    }
}

private int computeRight(int spanIndex, int sizeAvg) {
    if (spanIndex == mSpanCount - 1) {
        return mRight;
    } else if (spanIndex >= mSpanCount / 2) {
        //從右邊算起
        return mHorizontal - computeLeft(spanIndex + 1, sizeAvg);
    } else {
        //從左邊算起
        return sizeAvg - computeLeft(spanIndex, sizeAvg);
    }
}

其實(shí)就是一個遞歸的算法璧亚,用的就是上面分析的邏輯,不清楚可以回去翻翻上面的圖脂信。計算出水平的 offsets 后癣蟋,后面的就很簡單了,接下來會判斷是否第一行跟最后一行來設(shè)置最頂部 top 跟最底部 bottom 狰闪。

這個GridSpaceDecoration就算完成了疯搅,主要就是完成一個 offsets 的設(shè)置,如果想要自定義一些分割線的效果埋泵,可以繼承此類并實(shí)現(xiàn) onDraw 方法即可幔欧。

4. (StaggeredLayoutManger) 瀑布流布局下均分等距間距(分割線)

(1) 實(shí)現(xiàn)效果

image

(3) 具體實(shí)現(xiàn)

這個實(shí)現(xiàn)跟上面的基本差不多,所以貼一下代碼就好了:

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                           @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    RecyclerView.LayoutManager originalManager = parent.getLayoutManager();
    if (originalManager == null || !(originalManager instanceof StaggeredGridLayoutManager)) {
        return;
    }
    StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) originalManager;
    if (manager.getOrientation() == StaggeredGridLayoutManager.VERTICAL) {
        handleVertical(outRect, view, parent);
    } else {
        handleHorizontal(outRect, view, parent);
    }
}

private void handleVertical(@NonNull Rect outRect, @NonNull View view,
                            @NonNull RecyclerView parent) {
    StaggeredGridLayoutManager.LayoutParams params =
            (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
    int spanIndex = params.getSpanIndex();
    int adapterPos = parent.getChildAdapterPosition(view);
    int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
    int left = computeLeft(spanIndex, sizeAvg);
    int right = computeRight(spanIndex, sizeAvg);
    outRect.left = left;
    outRect.right = right;
    outRect.top = mVertical / 2;
    outRect.bottom = mVertical / 2;
    if (isFirstRaw(adapterPos, spanIndex)) {
        //第一行
        outRect.top = mTop;
    }
    if (isLastRaw(spanIndex)) {
        //最后一行
        outRect.bottom = mBottom;
    }
}

5. (GridLayoutManager) 網(wǎng)格布局下實(shí)現(xiàn)表格式邊框

StaggeredSpaceDecoration

(1) 實(shí)現(xiàn)效果

image

(2) 具體實(shí)現(xiàn)

TableDecoration

TableDecoration 是繼承于上面第3點(diǎn)的 GridSpaceDecoration來實(shí)現(xiàn)的丽声,GridSpaceDecoration 負(fù)責(zé)間距處理礁蔗,TableDecoration 則是將分割線給畫出來。所以主要就是 onDraw 方法的實(shí)現(xiàn):

先看構(gòu)造方法:

public class TableDecoration extends GridSpaceDecoration {

    private Drawable mDivider;
    private int mSize;
    private Rect mBounds;

    /**
     * @param color 邊框顏色
     * @param size 邊框大醒闵纭(px)
     */
    public TableDecoration(@ColorInt int color, int size) {
        super(size, size, size, size, size, size);
        mSize = size;
        mDivider = new ColorDrawable(color);
        mBounds = new Rect();
    }
}

就是將 item 的所有邊框都設(shè)置為 size ,然后根據(jù)傳進(jìn)來的 color 創(chuàng)建一個 Drawable 對象浴井。接著看 onDraw方法:

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View view = parent.getChildAt(i);
        draw(c, parent, view);
    }
    drawLast(c, parent);
}

先是遍歷所有 child ,然后進(jìn)行每個 child 的繪制:

private void draw(Canvas canvas, RecyclerView parent, View view) {
    canvas.save();
    int translationX = Math.round(view.getTranslationX());
    int translationY = Math.round(view.getTranslationY());
    int viewLeft = view.getLeft() + translationX;
    int viewRight = view.getRight() + translationX;
    int viewTop = view.getTop() + translationY;
    int viewBottom = view.getBottom() + translationY;
    parent.getDecoratedBoundsWithMargins(view, mBounds);
    drawLeft(canvas, mBounds, viewLeft);
    drawRight(canvas, mBounds, viewRight);
    drawTop(canvas, mBounds, viewTop);
    drawBottom(canvas, mBounds, viewBottom);
    canvas.restore();
}

private void drawLeft(Canvas canvas, Rect bounds, int left) {
    mDivider.setBounds(bounds.left, bounds.top, left, bounds.bottom);
    mDivider.draw(canvas);
}
//...

邏輯也不難,跟第2點(diǎn) 自定義分割線實(shí)現(xiàn) 里的邏輯差不多霉撵,將我們設(shè)置的 item 的所有間距畫出來磺浙,這里就不細(xì)說了。畫完所有 item 后徒坡,還會在 onDraw 調(diào)用一個 drawLast 方法撕氧,我們先看看沒有調(diào)用這個方法是怎樣的效果:

image

可以很明顯看出,最后那里如果 item 不是鋪滿整一行的話喇完,會導(dǎo)致后面那里有一部分的缺陷伦泥,這個缺陷其實(shí)我們在第3點(diǎn) 網(wǎng)格布局下的均分等距間距(分割線)GridSpaceDecoration 時分析過程中就可以發(fā)現(xiàn)了,由于每個 item 的上下左右 offsets 并不一定一致,所以會導(dǎo)致當(dāng)沒有最后一行有空缺的話就會造成一個邊框的缺陷不脯。

原因了解了府怯,那么問題解決應(yīng)該也不難:

private void drawLast(Canvas canvas, RecyclerView parent) {
    View lastView = parent.getChildAt(parent.getChildCount() - 1);
    int pos = parent.getChildAdapterPosition(lastView);
    if (isLastColumn((GridLayoutManager.LayoutParams) lastView.getLayoutParams(),pos)){
        return;
    }
    int translationX = Math.round(lastView.getTranslationX());
    int translationY = Math.round(lastView.getTranslationY());
    int viewLeft = lastView.getLeft() + translationX;
    int viewRight = lastView.getRight() + translationX;
    int viewTop = lastView.getTop() + translationY;
    int viewBottom = lastView.getBottom() + translationY;
    parent.getDecoratedBoundsWithMargins(lastView, mBounds);
    canvas.save();
    if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
        int contentRight = parent.getRight() - parent.getPaddingRight() - Math.round(parent.getTranslationX());
        //空白區(qū)域上邊緣
        mDivider.setBounds(mBounds.right, mBounds.top, contentRight, viewTop);
        mDivider.draw(canvas);
        //空白區(qū)域左邊緣
        mDivider.setBounds(viewRight, viewTop, viewRight + mSize, mBounds.bottom);
        mDivider.draw(canvas);
    }else {
        int contentBottom = parent.getBottom()-parent.getPaddingBottom()-Math.round(parent.getTranslationY());
        //空白區(qū)域上邊緣
        mDivider.setBounds(mBounds.left,viewBottom,mBounds.right,viewBottom+mSize);
        mDivider.draw(canvas);
        //空白區(qū)域左邊緣
        mDivider.setBounds(mBounds.left,mBounds.bottom,viewLeft,contentBottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}

主要邏輯就是將空缺出來的地方給補(bǔ)齊。

6. (GridLayoutManager) 打造粘性頭部

StickHeaderDecoration

(1) 實(shí)現(xiàn)效果

image

(2) 具體實(shí)現(xiàn)

  • 分析

    上面的幾個例子中跨新,getItemOffsets 以及 onDraw 方法都用過了,Decoration 中三大方法還有一個 onDrawOver,這個效果就是用 onDrawOver來實(shí)現(xiàn)的坏逢。

    邏輯是這樣的:要實(shí)現(xiàn)這樣的效果域帐,我們需要在 RecyclerView 的頂部畫上一個 StickHeader,也就是我們的第一個 Child。 同時也有一個問題就是我們怎么知道哪個 item 是可以當(dāng)成頭部(StickHeader)的是整,這里我提供了一個接口來進(jìn)行判斷:

    public interface StickProvider {
        boolean isStick(int position);
    }
    

    這是 StickHeaderDecoration 的一個內(nèi)部實(shí)現(xiàn)類肖揣,需要將它的一個對象作為 StickHeaderDecoration的構(gòu)造方法的參數(shù),例如:

    StickHeaderDecoration decoration = new StickHeaderDecoration(new StickHeaderDecoration.StickProvider() {
    @Override
    public boolean isStick(int position) {
        return mList.get(position).type == StickBean.TYPE_HEADER;
    }
    });
    
    //使用labamda會更簡潔
    StickHeaderDecoration decoration = 
        new StickHeaderDecoration(position -> mList.get(position).type == StickBean.TYPE_HEADER);
    

    然后我們就可以通過這個StickProvider對象進(jìn)行判斷是否是需要顯示的頭部了浮入,接著看主要的方法onDrawOver:

    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                           @NonNull RecyclerView.State state) {
        RecyclerView.Adapter adapter = parent.getAdapter();
        if (adapter == null || !(adapter instanceof StickProvider)) {
            return;
        }
        int itemCount = adapter.getItemCount();
        if (itemCount == 1) {
            return;
        }
        //找到當(dāng)前的StickHeader對應(yīng)的position
        int currStickPos = currStickPos(parent);       //注釋1
        if (currStickPos == -1) {
            return;
        }
        c.save();
        if (parent.getClipToPadding()) {
            //考慮padding的情況
            c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(),
                    parent.getWidth() - parent.getPaddingRight(),
                    parent.getHeight() - parent.getPaddingBottom());
        }
        int currStickType = adapter.getItemViewType(currStickPos);
        //當(dāng)前顯示的StickHeader相應(yīng)的ViewHolder龙优,先看有沒有緩存
        RecyclerView.ViewHolder currHolder = mViewMap.get(currStickType);
        if (currHolder == null) {
            //沒有緩存則新生成
            currHolder = adapter.createViewHolder(parent, currStickType);
            //主動測量并布局
            measure(currHolder.itemView, parent);
            mViewMap.put(currStickType, currHolder);
        }
        adapter.bindViewHolder(currHolder, currStickPos);
        c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
        currHolder.itemView.draw(c);
        c.restore();
    }
    
    

    整體邏輯并不難,先是找到當(dāng)前要顯示的頭部事秀,這個頭部怎么來的呢彤断,看看注釋1處的 currStickPos 方法:

    private int currStickPos(RecyclerView parent) {
        int childCount = parent.getChildCount();
        int paddingTop = parent.getPaddingTop();
        int currStickPos = -1;
        for (int i = 0; i < childCount; i++) {
            //考慮到parent padding 的情況,第一個item有可能不可見情況
            //從第1個child向后找
            View child = parent.getChildAt(i);
            if (child.getTop() >= paddingTop) {
                break;
            }
            int pos = parent.getChildAdapterPosition(child);
            if (mProvider.isStick(pos)) {
                currStickPos = pos;
            }
        }
        if (currStickPos != -1) {
            return currStickPos;
        }
        for (int i = parent.getChildAdapterPosition(parent.getChildAt(0)) - 1; i >= 0; i--) {
            //從第一個child的前一個開始找
            if (mProvider.isStick(i)) {
                return i;
            }
        }
        return -1;
    }
    

    主要邏輯分為兩步:

    • 因?yàn)楫?dāng) RecyclerView 設(shè)置 paddingTop 時易迹,第一個 Item 有可能是不可見的(被padding蓋住了)宰衙,所以第一步是從當(dāng)前第一個 child 開始向后找(child的top<paddingTop),當(dāng)找到時則返回對應(yīng)的 Adapter position,如果沒有找到睹欲,則進(jìn)行二步供炼。
    • 第二步就是從第一個child的 Adapter 前一個 position 開始找,找到則返回窘疮,如果都沒找到袋哼,則返回-1。

    再回到 onDrawOver 方法中闸衫,當(dāng)找到當(dāng)前要顯示的 Header 后涛贯,并會為他進(jìn)行測量,然后布局(具體看項(xiàng)目源碼),接著再調(diào)用 Adapter 的 bindViewHolder方法進(jìn)行數(shù)據(jù)綁定蔚出,最后再畫出來就ok了疫蔓,接著看看效果:

image

看到效果圖并不是我們想要達(dá)到的效果,很明顯缺少一個推動的效果身冬,那么這個怎么實(shí)現(xiàn)呢:

@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                    @NonNull RecyclerView.State state) {
 //...
 
 //尋找下一個StickHeader
 RecyclerView.ViewHolder nextStickHolder = nextStickHolder(parent, currStickPos);
 if (nextStickHolder != null) {
     RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) currHolder.itemView.getLayoutParams();
     int bottom = parent.getPaddingTop() + params.topMargin + currHolder.itemView.getMeasuredHeight();
     int nextStickTop = nextStickHolder.itemView.getTop();
     //下一個StickHeader如果頂部碰到了當(dāng)前StickHeader的屁股衅胀,那么將當(dāng)前的向上推
     if (nextStickTop < bottom && nextStickTop > 0) {
         c.translate(0, nextStickTop - bottom);
     }
 }
 adapter.bindViewHolder(currHolder, currStickPos);
 c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
 currHolder.itemView.draw(c);
 c.restore();
}

邏輯也不難,就是找到下一個 Header 酥筝,如果它碰到了上面那個的屁股的話滚躯,就將上面那個向上移動一點(diǎn),就可以形成我們的推動效果啦。

三 總結(jié)

從決定說要學(xué)習(xí)這個開始掸掏,到寫完Demo,寫完文章茁影,大概花了2個星期,其中有一些點(diǎn)也是深入了解了部分源碼丧凤,掉了不少頭發(fā)才總結(jié)出來募闲。其中也碰到不少坑,而且這個系列目前網(wǎng)上的文章比較雜愿待,很少有一個整體的分析浩螺,甚至有一些理解是錯的,所以這篇文章寫了相對詳細(xì)很多仍侥。

由于編者水平有限要出,文章難免會有錯漏的地方,如有發(fā)現(xiàn)农渊,懇請指正患蹂,如果有更好的實(shí)現(xiàn)思路也可以提供。

要看項(xiàng)目源碼或者Demo的戳這里

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砸紊,一起剝皮案震驚了整個濱河市传于,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌醉顽,老刑警劉巖格了,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異徽鼎,居然都是意外死亡盛末,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進(jìn)店門否淤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悄但,“玉大人,你說我怎么就攤上這事石抡¢芟” “怎么了?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵啰扛,是天一觀的道長嚎京。 經(jīng)常有香客問我,道長隐解,這世上最難降的妖魔是什么鞍帝? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮煞茫,結(jié)果婚禮上帕涌,老公的妹妹穿的比我還像新娘摄凡。我一直安慰自己,他們只是感情好蚓曼,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布亲澡。 她就那樣靜靜地躺著,像睡著了一般纫版。 火紅的嫁衣襯著肌膚如雪床绪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天其弊,我揣著相機(jī)與錄音癞己,去河邊找鬼。 笑死瑞凑,一個胖子當(dāng)著我的面吹牛末秃,可吹牛的內(nèi)容都是我干的概页。 我是一名探鬼主播籽御,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼惰匙!你這毒婦竟也來了技掏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤项鬼,失蹤者是張志新(化名)和其女友劉穎哑梳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绘盟,經(jīng)...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸠真,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了龄毡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吠卷。...
    茶點(diǎn)故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沦零,靈堂內(nèi)的尸體忽然破棺而出祭隔,到底是詐尸還是另有隱情,我是刑警寧澤路操,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布疾渴,位于F島的核電站,受9級特大地震影響屯仗,放射性物質(zhì)發(fā)生泄漏搞坝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一魁袜、第九天 我趴在偏房一處隱蔽的房頂上張望瞄沙。 院中可真熱鬧己沛,春花似錦、人聲如沸距境。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垫桂。三九已至师幕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诬滩,已是汗流浹背霹粥。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疼鸟,地道東北人后控。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像空镜,于是被迫代替她去往敵國和親浩淘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內(nèi)容