一 概述
這是這個系列的第二篇文章,第一篇 ItemDecoration深入解析與實戰(一)——原始碼分析 是偏原理性的,而這篇是偏應用性的。沒看過上一篇文章對閱讀此文也基本沒多大影響,不過了解原理會加深對本文Demo的理解。
這篇文章將會實現上篇文章最後說的幾個實戰點,包括:
- (
LinearLayoutManager
) 最簡單的分割線實現 - (
LinearLayoutManager
) 自定義分割線實現 - (
GridLayoutManager
) 網格佈局下的均分等距間距(分割線) - (
StaggeredLayoutManger
) 瀑布流佈局下均分等距間距(分割線) - (
GridLayoutManager
) 網格佈局下實現表格式邊框 - 打造粘性頭部
看完這6點標題,應該會知道這篇文章的篇幅會稍長,不過因為是實戰型別的文章,所以也不會特別枯燥。
建議
1. 你需要具備怎樣的前提知識
- 閱讀本文應該有一定的 RecyclerView 使用基礎
- 對 View 的基礎繪製使用有了解(沒有影響也不大)
2. 閱讀順序
- 從頭到尾,這有個難易順序,讀下去會比較順暢
- 由於文章較長,可以挑上面6點其中一個感興趣的進行閱讀,拉到下方每個點的第一部分都會有一個實現圖,可以觀看實際效果決定是否想要閱讀
二 實戰
1. (LinearLayoutManager
) 最簡單的分割線實現
(1) 實現效果
(2) 具體實現
像這種單一顏色的分割線實現起來很簡單,就是一行程式碼:
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,就會變成這樣:
就是說萬一我們的需求是有padding,而且背景顏色要跟分割線顏色不同那就沒辦法了。如果要解決這一問題,就要看第2點。
2. (LinearLayoutManager
) 自定義分割線實現
(1) 實現效果
(2) 使用
由於 support 包中已經有了一個預設的實現,所以就沒有自己寫了,這是官方自帶的 ItemDecoration
實現類,先看下怎麼用:
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
就是上圖的一個分割線效果,如果沒有設定這個,那麼將會有一個預設的灰色分割線:
<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) 實現效果
效果如上圖,解決了下面的常見問題:
- 某些 item 佔用多個 span 情況
- item 之前的間距相等
- item 的寬高可以保持一致,不會有某個 item 被壓扁的情況
- 上下左右的邊框可以與中間的分割線寬度不一致,每個都可以單獨設定
(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: 分析
要實現的功能很清晰,就是要解決上面的常見問題。其中,第2、3點比較麻煩,為什麼呢?先分析一下
先看下上圖,當使用 GridLayoutManager
時,GridLayoutManager
會將每個 Item 的最大可用空間平均分配開來,就像上圖黑線所對應的三個框就是3個 Item 的最大可分配空間。橙色區域就是 Decoration 設定的值跟 item 的 margin ,如果 margin 為0,那麼橙色區域便是在 getItemOffsets
方法中設定的值(下面簡稱 offsets)。綠色虛線所圍成的區域就是我們 itemView 的實際空間。
通過上圖,當我們為 item 設定相同的間距時,會發現 item 1 的空間被壓縮了,那麼怎麼解決這一問題呢?
- 每個item 寬度相同
- 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的戳這裡