ItemDecoration深刻解析與實戰(二)—— 實際運用

一 概述

這是這個系列的第二篇文章,第一篇 ItemDecoration深刻解析與實戰(一)——源碼分析 是偏原理性的,而這篇是偏應用性的。沒看過上一篇文章對閱讀此文也基本沒多大影響,不過了解原理會加深對本文Demo的理解。java

這篇文章將會實現上篇文章最後說的幾個實戰點,包括:android

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

看完這6點標題,應該會知道這篇文章的篇幅會稍長,不過由於是實戰類型的文章,因此也不會特別枯燥。git

建議

1. 你須要具有怎樣的前提知識

  • 閱讀本文應該有必定的 RecyclerView 使用基礎
  • 對 View 的基礎繪製使用有了解(沒有影響也不大)

2. 閱讀順序

  • 從頭至尾,這有個難易順序,讀下去會比較順暢
  • 因爲文章較長,能夠挑上面6點其中一個感興趣的進行閱讀,拉到下方每一個點的第一部分都會有一個實現圖,能夠觀看實際效果決定是否想要閱讀

二 實戰

1. (LinearLayoutManager) 最簡單的分割線實現

(1) 實現效果

(2) 具體實現

像這種單一顏色的分割線實現起來很簡單,就是一行代碼:github

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對應的就是outRect.bottom,看過這系列的上篇文章就能容易理解,這個跟在 ItemView的佈局文件中增長一個 marginBottom是同樣的效果的。不過這樣默認是沒有顏色的,這個分割線的顏色就取決於 RecyclerView的背景顏色。如咱們的效果圖的實現:算法

RecyclerView rvTest = findViewById(R.id.rv_test);
rvTest.addItemDecoration(new SimpleDividerDecoration());
複製代碼

這種實現很簡單,可是缺點也很突出,由於他是依賴於 RecyclerView 的背景的,而若是咱們爲 RecyclerView 設置一個padding,就會變成這樣:canvas

就是說萬一咱們的需求是有padding,並且背景顏色要跟分割線顏色不一樣那就沒辦法了。若是要解決這一問題,就要看第2點。緩存

2. (LinearLayoutManager) 自定義分割線實現

(1) 實現效果

(2) 使用

因爲 support 包中已經有了一個默認的實現,因此就沒有本身寫了,這是官方自帶的 ItemDecoration實現類,先看下怎麼用:bash

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 就是上圖的一個分割線效果,若是沒有設置這個,那麼將會有一個默認的灰色分割線:ide

<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) 具體實現

用法很簡單,但正所謂知其然,還要知其因此然,咱們看一下這個 DividerItemDecoration 裏面的具體實現是怎樣的:

  • 先看getItemOffsets方法的具體實現

// 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個實戰點中最簡單的那種方式,只不過他的高度變成了mDivider.getIntrinsicHeight()而已,這個mDivider就是咱們 setDrawable中設置的一個 Drawable 對象,若是沒有設置,那就會有一個默認的。

  • 再看 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 最大可用空間的左右邊界,若是咱們沒有設置,parent.getClipToPadding() 默認是返回 ture 的,即最大可用空間的要減去RecyclerView的padding,這是爲了讓padding不被分割線覆蓋。

再看註釋分割線的下邊,這裏遍歷了全部的 Child 。先看註釋1這句代碼,parent.getDecoratedBoundsWithMargins(child, mBounds),這個方法有什麼用呢,其實看名稱就能大概猜出來,這個方法能夠拿到

child邊界+decoration + margin

所組成的Rect的邊界值mbounds,即下圖裏面的橙色區域的外邊框所對應的值。

注意:此圖不嚴謹,詳細內容請看這系列的上一篇文章

而後便會將mbounds的 bottom 跟 top ,以及 上面獲得的 left 跟 right 設置到 mDivider的邊界中,就得到的咱們上圖的紅色虛線邊框的矩形,若是咱們沒有爲 itemView 設置 margin,那麼就會獲得綠色虛線邊框的範圍,再將這部分畫出來,就獲得了咱們想要的分割線了。

3. (GridLayoutManager) 網格佈局下的均分等距間距(分割線)

GridSpaceDecoration

(1) 實現效果

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

  1. 某些 item 佔用多個 span 狀況
  2. item 以前的間距相等
  3. item 的寬高能夠保持一致,不會有某個 item 被壓扁的狀況
  4. 上下左右的邊框能夠與中間的分割線寬度不一致,每一個均可以單獨設置

(2) 使用方法

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

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

/**
 * @param horizontal 內部水平距離(px)
 * @param vertical   內部豎直距離(px)
 * @param left       最左邊距離(px),默認爲0
 * @param right      最右邊距離(px),默認爲0
 * @param top        最頂端距離(px),默認爲0
 * @param bottom     最底端距離(px),默認爲0
 */
public GridSpaceDecoration(int horizontal, int vertical, int left, int right, int top, int bottom){
    //...
}
複製代碼

該類提供了三個構造方法,直接設置相應的值,而後 add 到 RecyclerView中便可。

(3) 具體實現

step1: 分析

要實現的功能很清晰,就是要解決上面的常見問題。其中,第二、3點比較麻煩,爲何呢?先分析一下

先看下上圖,當使用 GridLayoutManager 時,GridLayoutManager會將每一個 Item 的最大可用空間平均分配開來,就像上圖黑線所對應的三個框就是3個 Item 的最大可分配空間。橙色區域就是 Decoration 設置的值跟 item 的 margin ,若是 margin 爲0,那麼橙色區域即是在 getItemOffsets 方法中設置的值(下面簡稱 offsets)。綠色虛線所圍成的區域就是咱們 itemView 的實際空間。

經過上圖,當咱們爲 item 設置相同的間距時,會發現 item 1 的空間被壓縮了,那麼怎麼解決這一問題呢?

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

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

  • 先討論第1點,由於每一個 item 的最大可用空間(黑色框格子)是一致的,因此想要讓 item 的寬度同樣,就是讓每一個 item 的 offsets 保持一致。咱們能夠獲得下面的公式:

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

    其中,left 、right 爲最左、左右邊間距,center 爲中間間距,spanCount 爲每一行的 span 個數,就能夠得出每一個 item 須要設置的 offsets 大小 sizeAvg,這樣就能夠保證每一個 item 的寬度一致(均分)

  • 再看第2點,咱們要保證每一箇中間間距都同樣,左右間距達到咱們設置的大小。首先,最左邊的間距是已經肯定了的,即 left,那麼最左邊 item 的右邊 right1 就能夠得出爲 sizeAvg - left,第二個 item 左邊間距 left2 就是 center - right1 同理能夠推出接下來的 item ,看下圖會更清晰:

    而後把中間的實體線給去掉:

    就能夠看到每一個 item 的寬度同樣了,並且間距也是符合預期的效果。(圖片是人工畫的,可能會有點小偏差)

step2 實現

上面分析完成,接着看看算法實現:

@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);
    }
}
複製代碼

很簡單,先是作了一點初始化,而後分兩個方向進行不一樣處理。直接看註釋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,而後再調用 computeLeft 方法(註釋1),先看下這個方法這怎樣的實現:

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);
    }
}
複製代碼

其實就是一個遞歸的算法,用的就是上面分析的邏輯,不清楚能夠回去翻翻上面的圖。計算出水平的 offsets 後,後面的就很簡單了,接下來會判斷是否第一行跟最後一行來設置最頂部 top 跟最底部 bottom 。

這個GridSpaceDecoration就算完成了,主要就是完成一個 offsets 的設置,若是想要自定義一些分割線的效果,能夠繼承此類並實現 onDraw 方法便可。

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

(1) 實現效果

(3) 具體實現

這個實現跟上面的基本差很少,因此貼一下代碼就行了:

@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) 網格佈局下實現表格式邊框

StaggeredSpaceDecoration

(1) 實現效果

(2) 具體實現

TableDecoration

TableDecoration 是繼承於上面第3點的 GridSpaceDecoration來實現的,GridSpaceDecoration 負責間距處理,TableDecoration 則是將分割線給畫出來。因此主要就是 onDraw 方法的實現:

先看構造方法:

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 的全部邊框都設置爲 size ,而後根據傳進來的 color 建立一個 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 ,而後進行每一個 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點 自定義分割線實現 裏的邏輯差很少,將咱們設置的 item 的全部間距畫出來,這裏就不細說了。畫完全部 item 後,還會在 onDraw 調用一個 drawLast 方法,咱們先看看沒有調用這個方法是怎樣的效果:

能夠很明顯看出,最後那裏若是 item 不是鋪滿整一行的話,會致使後面那裏有一部分的缺陷,這個缺陷其實咱們在第3點 網格佈局下的均分等距間距(分割線)GridSpaceDecoration 時分析過程當中就能夠發現了,因爲每一個 item 的上下左右 offsets 並不必定一致,因此會致使當沒有最後一行有空缺的話就會形成一個邊框的缺陷。

緣由瞭解了,那麼問題解決應該也不難:

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());
        //空白區域上邊緣
        mDivider.setBounds(mBounds.right, mBounds.top, contentRight, viewTop);
        mDivider.draw(canvas);
        //空白區域左邊緣
        mDivider.setBounds(viewRight, viewTop, viewRight + mSize, mBounds.bottom);
        mDivider.draw(canvas);
    }else {
        int contentBottom = parent.getBottom()-parent.getPaddingBottom()-Math.round(parent.getTranslationY());
        //空白區域上邊緣
        mDivider.setBounds(mBounds.left,viewBottom,mBounds.right,viewBottom+mSize);
        mDivider.draw(canvas);
        //空白區域左邊緣
        mDivider.setBounds(mBounds.left,mBounds.bottom,viewLeft,contentBottom);
        mDivider.draw(canvas);
    }
    canvas.restore();
}
複製代碼

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

6. (GridLayoutManager) 打造粘性頭部

StickHeaderDecoration

(1) 實現效果

(2) 具體實現

  • 分析

    上面的幾個例子中,getItemOffsets 以及 onDraw 方法都用過了,Decoration 中三大方法還有一個 onDrawOver,這個效果就是用 onDrawOver來實現的。

    邏輯是這樣的:要實現這樣的效果,咱們須要在 RecyclerView 的頂部畫上一個 StickHeader,也就是咱們的第一個 Child。 同時也有一個問題就是咱們怎麼知道哪一個 item 是能夠當成頭部(StickHeader)的,這裏我提供了一個接口來進行判斷:

    public interface StickProvider {
        boolean isStick(int position);
    }
    複製代碼

    這是 StickHeaderDecoration 的一個內部實現類,須要將它的一個對象做爲 StickHeaderDecoration的構造方法的參數,例如:

    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對象進行判斷是不是須要顯示的頭部了,接着看主要的方法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;
        }
        //找到當前的StickHeader對應的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);
        //當前顯示的StickHeader相應的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();
    }
    
    複製代碼

    總體邏輯並不難,先是找到當前要顯示的頭部,這個頭部怎麼來的呢,看看註釋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;
    }
    複製代碼

    主要邏輯分爲兩步:

    • 由於當 RecyclerView 設置 paddingTop 時,第一個 Item 有多是不可見的(被padding蓋住了),因此第一步是從當前第一個 child 開始向後找(child的top<paddingTop),當找到時則返回對應的 Adapter position,若是沒有找到,則進行二步。
    • 第二步就是從第一個child的 Adapter 前一個 position 開始找,找到則返回,若是都沒找到,則返回-1。

    再回到 onDrawOver 方法中,當找到當前要顯示的 Header 後,並會爲他進行測量,而後佈局(具體看項目源碼),接着再調用 Adapter 的 bindViewHolder方法進行數據綁定,最後再畫出來就ok了,接着看看效果:

看到效果圖並非咱們想要達到的效果,很明顯缺乏一個推進的效果,那麼這個怎麼實現呢:

@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若是頂部碰到了當前StickHeader的屁股,那麼將當前的向上推
     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 ,若是它碰到了上面那個的屁股的話,就將上面那個向上移動一點,就能夠造成咱們的推進效果啦。

三 總結

從決定說要學習這個開始,到寫完Demo,寫完文章,大概花了2個星期,其中有一些點也是深刻了解了部分源碼,掉了很多頭髮才總結出來。其中也碰到很多坑,並且這個系列目前網上的文章比較雜,不多有一個總體的分析,甚至有一些理解是錯的,因此這篇文章寫了相對詳細不少。

因爲編者水平有限,文章不免會有錯漏的地方,若有發現,懇請指正,若是有更好的實現思路也能夠提供。

要看項目源碼或者Demo的戳這裏

相關文章
相關標籤/搜索