RecyclerView探索之通過ItemDecoration實現StickyHeader效果
我在上一篇《小甜點,RecyclerView 之 ItemDecoration 講解及高階特性實踐 》 講解了 ItemDecoration 的基本用法及它的一些實踐,抱著學習研究的態度,這一篇作為實踐篇主要目的是嘗試通過 ItemDecoration 來實現 RecyclerView 中的 StickyHeader 功能。
關於 StickyHeader 想必大家已經很清楚了,如果不有不清楚的,看下圖:
如果要實現 StickyHeader 的話,首先,我們得明白普通的 Header 是怎麼實現的。
ItemDecoration 實現普通的 Header
上面這張圖是我微信的通訊錄介面,大家可以看到微信按拼音和英文名首字母給賬號進行了分組,上面灰色的 B 和 C 就是 Header。
之前在 ListView 時代,實現頭部功能就是通過 ItemView 的 layout 佈局實現的。
一個 ItemView 分為兩個部分,如果這個 ItemView 是小組的第一個,那麼它的 Header 就應該顯示出來,不然就得隱藏,所以只要好處理分組與 ItemView 的位置關係,這個 Header 功能就很容易實現了。
現在,用 ItemDecoration 來實現頭部,就不需要在每個 ItemView 中設定這個隱藏的 Header 部分了,ItemView 只需要關心它自己真正要表現的介面效果就好了,像這種零碎的事情就專門交給 ItemDecoration 來處理。
但不管是 ItemView 還是 ItemDecoration 來實現 Header,正確的資料分組永遠是第一步。
而資料的分組離不開 Adapter 的配合,所以資料的分組應該由外部來完成,而不是 ItemDecoration 本身,那好,建立 ItemDecoration 第一步就是定義一個介面,用來獲取分組資訊。
public class GroupInfo {
//組號
private int mGroupID;
// Header 的 title
private String mTitle;
public GroupInfo(int groupId, String title) {
this.mGroupID = groupId;
this.mTitle = title;
}
public int getGroupID() {
return mGroupID;
}
public void setGroupID(int groupID) {
this.mGroupID = groupID;
}
public String getTitle() {
return mTitle;
}
public void setTitle(String title) {
this.mTitle = title;
}
}
上面程式碼 Header 的相關資訊。
public class SectionDecoration extends RecyclerView.ItemDecoration {
public interface GroupInfoCallback {
GroupInfo getGroupInfo(int position);
}
}
有了 GroupInfoCallback 回撥,SectionItemDecoration 就可以通過它的 getGroupInfo() 方法來獲取每個 ItemView 對應的分組資訊。
我們再回到 Header 話題上來,因為是通過 ItemDecoration 來完成它,所以肯定要藉助於它的 getItemOffsets() 方法。我們組與組之間的間隔設定成為一個 Header 的高度,然後組內的 ItemView 之間的間距是指定的間距值,通常為 1 px 或者 2 px。大家看圖就明白了。
這張圖與上面的那張差不多,但是灰色區域都是通過 ItemDecoration 中 getItemOffsets 方法操縱 outRect 引數撐開的。我們繪製 Header 只要計算出對應的位置然後通過 Canvas 就能為所欲為了。關鍵的一點在於 Header 只繪製在組內第一個 ItemView 的上方,所以我們還需要一個途徑來獲知 ItemView 在組內的位置。我們可以升級 GroupInfo 類,新增一個域用來標記 ItemView 在組內的位置,還需要提供一個方法來判斷它是不是組內的第一個。
public class GroupInfo {
//組號
private int mGroupID;
// Header 的 title
private String mTitle;
//ItemView 在組內的位置
private int position;
//程式碼有精簡
......
public boolean isFirstViewInGroup () {
return position == 0;
}
public void setPosition(int position) {
this.position = position;
}
}
並且 HeaderItemDecoration 只提供介面,實現邏輯交由外部。
public class SectionDecoration extends RecyclerView.ItemDecoration {
//程式碼有精簡
......
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = parent.getChildAdapterPosition(view);
if ( mCallback != null ) {
GroupInfo groupInfo = mCallback.getGroupInfo(position);
//如果是組內的第一個則將間距撐開為一個Header的高度,或者就是普通的分割線高度
if ( groupInfo != null && groupInfo.isFirstViewInGroup() ) {
outRect.top = mHeaderHeight;
} else {
outRect.top = mDividerHeight;
}
}
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
int childCount = parent.getChildCount();
for ( int i = 0; i < childCount; i++ ) {
View view = parent.getChildAt(i);
int index = parent.getChildAdapterPosition(view);
if ( mCallback != null ) {
GroupInfo groupinfo = mCallback.getGroupInfo(index);
//只有組內的第一個ItemView之上才繪製
if ( groupinfo.isFirstViewInGroup() ) {
int left = parent.getPaddingLeft();
int top = view.getTop() - mHeaderHeight;
int right = parent.getWidth() - parent.getPaddingRight();
int bottom = view.getTop();
//繪製Header
c.drawRect(left,top,right,bottom,mPaint);
float titleX = left + mTextOffsetX;
float titleY = bottom - mFontMetrics.descent;
//繪製Title
c.drawText(groupinfo.getTitle(),titleX,titleY,mTextPaint);
}
}
}
}
}
上面的程式碼,是實現 Header 的核心程式碼。
getItemOffsets 用來設定 ItemView 之間的間距,組內的第一個 View 之上會間隔出一個 Header 的高度,否則就是普通的分割線高度。
onDraw 用來遍歷螢幕上的 ItemView,通過獲取它們在 Adapter 中的位置,然後通過外部介面 GroupInfoCallback 得到它的組資訊 GroupInfo。通過判斷它是否是組內第一個 View 來決定是否在它之上繪製 Header。繪製的流程也非常簡單。先確定 Header 的 Rect 範圍,然後繪製,再在合適的位置上繪製上 Header 的 title。接下來要做的事情就是在 Activity 中去初始化 RecyclerView 相關。
/**初始化測試資料*/
private void initDatas() {
data = new ArrayList<>();
for (int i = 0; i < 56;i++) {
data.add(i+" test ");
}
}
initDatas();
mAdapter = new TestAdapter(data);
mRecyclerView.setAdapter(mAdapter);
LinearLayoutManager layoutmanager = new LinearLayoutManager(this);
layoutmanager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutmanager);
SectionDecoration.GroupInfoCallback callback = new SectionDecoration.GroupInfoCallback() {
@Override
public GroupInfo getGroupInfo(int position) {
/**
* 分組邏輯,這裡為了測試每5個數據為一組。大家可以在實際開發中
* 替換為真正的需求邏輯
*/
int groupId = position / 5;
int index = position % 5;
GroupInfo groupInfo = new GroupInfo(groupId,groupId+"");
groupInfo.setPosition(index);
return groupInfo;
}
};
mRecyclerView.addItemDecoration(new SectionDecoration(this,callback));
最終效果如下:
大家看到,程式碼很好的實現了效果。當然,這是測試程式,程式碼寫的粗糙,實際開發,可以根據條件對 Header 部分進行精細繪製,將測試資料替換成真實的資料。
實現了 Header 之後,我們繼續話題,接下來的任務是 StrickyHeader,它被稱為粘性頭部,或者懸停頭部,它和普通的 Header 不同的一點就是在組內的成員 ItemView 沒有徹底消失之前,它會懸停在頂部,像粘著不走的樣子,直到它下面的 Header 將它推走。語言比較抽象,大家看一眼真實的場景就明白了。
ItemDecoration 實現的 StickyHeader
從 Header 到 StickyHeader 看起來沒有改變多少,但開發難度卻實實在在提高了很多。
首先,api 的改變,之前通過 onDraw() 方法,就完成了 Header 的繪製,但是現在 StickyHeader 有懸停效果,看起來像是浮在 ItemView 內容之上,所以 onDraw() 方法不再合適,得用 onDrawOver()。
演算法邏輯不同。之前 Header 的繪製由組內第一個 ItemView 決定,但是 StickyHeader 由於懸停功能的新增,所以它是由螢幕上可見的組內的第一個 ItemView 來決定,每一個 ItemView 都有義務來繪製和維護StickyHeader 狀態。
我想到了一個關鍵詞:前赴後繼。
用一張圖片來加深大家的印象。大家可以想像一下,一個組的所有 ItemView 排隊去顯示 StickyHeader。有兩種情況需要考慮。
- 當前的 ItemView 不是螢幕上的第一個可見的 ItemView,但是它是組內的第一個 ItemView,所以這個時候按照繪製普通 Header 的邏輯繪製 StickyHeader 就可以了。
- 當前的 ItemView 不是螢幕上的第一個可見的 ItemView,同時它也不是組內的第一個 ItemView,所以它不需要做任何的事情。
- 當前的 ItemView 是螢幕上第一個可見的 ItemView,所以不管它是不是組內的第一個 ItemView,它都需要繪製 StickyHeader,因為它前面的兄弟陣亡了(滑動了視線外)。並且 StickyHeader 的起始位置應該依附在 RecyclerView 的內容起始位置,因為只有這樣才會表現出 StickyHeader 粘性懸停的效果。
好了,有了這面的邏輯,我們就可以根據差異性資訊來對前面的 Header 程式碼進行改造,在此基礎上打造 StickyHeader。
public class StickySectionDecoration extends RecyclerView.ItemDecoration {
//程式碼有精簡
......
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for ( int i = 0; i < childCount; i++ ) {
View view = parent.getChildAt(i);
int index = parent.getChildAdapterPosition(view);
if ( mCallback != null ) {
GroupInfo groupinfo = mCallback.getGroupInfo(index);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
//螢幕上第一個可見的 ItemView 時,i == 0;
if ( i != 0 ) {
//只有組內的第一個ItemView之上才繪製
if ( groupinfo.isFirstViewInGroup() ) {
int top = view.getTop() - mHeaderHeight;
int bottom = view.getTop();
drawHeaderRect(c, groupinfo, left, top, right, bottom);
}
} else {
//當 ItemView 是螢幕上第一個可見的View 時,不管它是不是組內第一個View
//它都需要繪製它對應的 StickyHeader。
int top = parent.getPaddingTop();
int bottom = top + mHeaderHeight;
drawHeaderRect(c, groupinfo, left, top, right, bottom);
}
}
}
}
private void drawHeaderRect(Canvas c, GroupInfo groupinfo, int left, int top, int right, int bottom) {
//繪製Header
c.drawRect(left,top,right,bottom,mPaint);
float titleX = left + mTextOffsetX;
float titleY = bottom - mFontMetrics.descent;
//繪製Title
c.drawText(groupinfo.getTitle(),titleX,titleY,mTextPaint);
}
....
同樣,我們在外面的 Activity 中初始化 RecyclerView。
StickySectionDecoration.GroupInfoCallback callback = new StickySectionDecoration.GroupInfoCallback() {
@Override
public GroupInfo getGroupInfo(int position) {
/**
* 分組邏輯,這裡為了測試每5個數據為一組。大家可以在實際開發中
* 替換為真正的需求邏輯
*/
int groupId = position / 5;
int index = position % 5;
GroupInfo groupInfo = new GroupInfo(groupId,groupId+"");
groupInfo.setPosition(index);
return groupInfo;
}
};
mRecyclerView.addItemDecoration(new StickySectionDecoration(this,callback));
效果如下:
可以看到,到這裡 StickyHeader 的懸停效果是完成了,但是大家仔細看,還有個細節是需要優化的。開篇說過,StickyHeader 懸停之後不消失,由下一個 StickyHeader 向上推走然後頂替它成為最頂層的 StickyHeader。
現在效果是:
理想的效果應該是這樣:
那麼,怎麼改進呢?
首先我們觀察一下現狀。
我們可以看到,現在最上面一個 Header 消失時,它是由下面的 Header 慢慢覆蓋的,我們理想的效果應該是下面的的 Header 快要到達頂部時,它向上推掉之前的 Header,然後取代它的位置。
那好,我們更進一步,思考下怎麼實現這個“推”的過程?回顧一下之前的程式碼。
//當 ItemView 是螢幕上第一個可見的View 時,不管它是不是組內第一個View
//它都需要繪製它對應的 StickyHeader。
int top = parent.getPaddingTop();
int bottom = top + mHeaderHeight;
drawHeaderRect(c, groupinfo, left, top, right, bottom);
我們看到,之前繪製最頂層的 Header 時,它的 Rect 範圍其實就已經固定了,緊貼著 parent 開始的地方,然後寬度為 parent 的寬度,高度為固定值。現在,我們要進行升級,實現一個“推”的動作。其實很簡單,讓 Header 跟隨組內最後一個 ItemView 一起移出螢幕就可以了。
我們現在需要考慮組內最後一個 ItemView 對 Header 的影響。
上面就是“推”這個動作的狀態分解。
- Section1 置於 RecyclerView 頂部,它的組內的 ItemView 由於向上滑動,從它的身下穿過,它的 top 值就是 parent.getPaddingTop()。
- Section1 置於 RecyclerView 頂部,由於Section2 的推擠,它組內最後一個 ItemView 的 bottom 已經和它的 bottom 一樣了,注意這個是臨界狀態了,Section1的 top 值還是 parent.getPaddingTop()。
- Section1 置於 RecyclerView 頂部,現在 Section1 的 bottom 值 與 它組內最後一個 ItemView 的 bottom 值是同一個。但是,它的 top 值不再是 parent.getPaddingTop()。而是它的最後一個 ItemView 的 bottom - mHeaderHeight。
- Section1 已經被推出了螢幕外面,Section2 已經取代它了,然後進入下一輪這樣的迴圈。
有了這些狀態分解,我們就可以輕鬆地寫程式碼了。關鍵一環就是如何確定某個 ItemView 是不是組內的最後一個 ItemView。所以,首先我們得升級我們的 GroupInfo 類。
public class GroupInfo {
//組號
private int mGroupID;
// Header 的 title
private String mTitle;
//ItemView 在組內的位置
private int position;
// 組的成員個數
private int mGroupLength;
// 程式碼有精簡
......
public GroupInfo(int groupId, String title) {
this.mGroupID = groupId;
this.mTitle = title;
}
public boolean isFirstViewInGroup () {
return position == 0;
}
public boolean isLastViewInGroup () {
return position == mGroupLength - 1 && position >= 0;
}
public void setGroupLength(int groupLength) {
this.mGroupLength = groupLength;
}
......
}
isLastViewInGroup()
方法就是 GroupInfo 提供的判斷是否是組內最後一個 ItemView 的依據。
我們再往下啃。看看最後一個組內的 ItemView 與 Header 之間的座標關係。
1. 正常情況而言,第一個 Header 它的座標值是固定的,所以它就表現出了懸浮的特性。
2. 當它組內最後一個 ItemView 的 bottom 值與 Header 的 bottom 一致時,也就是底部平齊的時候,view.getTop - mHeaderHeight 應該就是 Header 的 top 屬性理論取值。我們暫且用 Header.top 指代它。如果它的值小於 parent.getPaddingTop 的話,那麼 Header.top 就不能再為 parent.getPaddingTop 而應該是 view.getTop - mHeaderHeight 這個值,因為只有這樣才會形成 Header 與它組內最後一個 ItemView 一起滑出螢幕的效果,而下面一個 Header 因為緊挨著前一個組的最後一個 ItemView 的底部,所以造就了是新的 Header 快要到頂時推著之前的 Header 走的視覺效果。
大家再看看上面這張圖,細細體會一下。
我們可以接下來編寫程式碼了。我們只需要改變少許程式碼
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
int childCount = parent.getChildCount();
for ( int i = 0; i < childCount; i++ ) {
View view = parent.getChildAt(i);
int index = parent.getChildAdapterPosition(view);
if ( mCallback != null ) {
GroupInfo groupinfo = mCallback.getGroupInfo(index);
int left = parent.getPaddingLeft();
int right = parent.getWidth() - parent.getPaddingRight();
//螢幕上第一個可見的 ItemView 時,i == 0;
if ( i != 0 ) {
//只有組內的第一個ItemView之上才繪製
if ( groupinfo.isFirstViewInGroup() ) {
int top = view.getTop() - mHeaderHeight;
int bottom = view.getTop();
drawHeaderRect(c, groupinfo, left, top, right, bottom);
}
} else {
//當 ItemView 是螢幕上第一個可見的View 時,不管它是不是組內第一個View
//它都需要繪製它對應的 StickyHeader。
// 還要判斷當前的 ItemView 是不是它組內的最後一個 View
int top = parent.getPaddingTop();
if ( groupinfo.isLastViewInGroup() ) {
int suggestTop = view.getBottom() - mHeaderHeight;
// 當 ItemView 與 Header 底部平齊的時候,判斷 Header 的頂部是否小於
// parent 頂部內容開始的位置,如果小於則對 Header.top 進行位置更新,
//否則將繼續保持吸附在 parent 的頂部
if ( suggestTop < top ) {
top = suggestTop;
}
}
int bottom = top + mHeaderHeight;
drawHeaderRect(c, groupinfo, left, top, right, bottom);
}
}
}
}
編寫測試程式碼,然後檢視效果。
StickySectionDecoration.GroupInfoCallback callback = new StickySectionDecoration.GroupInfoCallback() {
@Override
public GroupInfo getGroupInfo(int position) {
/**
* 分組邏輯,這裡為了測試每5個數據為一組。大家可以在實際開發中
* 替換為真正的需求邏輯
*/
int groupId = position / 5;
int index = position % 5;
GroupInfo groupInfo = new GroupInfo(groupId,groupId+"");
groupInfo.setGroupLength(5);
groupInfo.setPosition(index);
return groupInfo;
}
};
mRecyclerView.addItemDecoration(new StickySectionDecoration(this,callback));
總結
其實通過 ItemDecoration 來實現 StickyHeader 是比較容易的。主要有下面幾個點:
1. 資料分組,分類。
2. 定義好資料分組的邏輯程式碼。
3. 編寫自定義的 ItemDecoration,處理好 getItemOffsets 方法中 Header 的繪製範圍。
4. 根據是否是第一個 Header 決定 Header 是否懸停。
5. 當 Header 是懸停效果時,要注意它與組內最後一個 ItemView 的位置關係。
附錄
相關推薦
RecyclerView探索之通過ItemDecoration實現StickyHeader效果
我在上一篇《小甜點,RecyclerView 之 ItemDecoration 講解及高階特性實踐 》 講解了 ItemDecoration 的基本用法及它的一些實踐,抱著學習研究的態度,這一篇作為實踐篇主要目的是嘗試通過 ItemDecoration 來實現
使用RecyclerView的ItemDecoration實現StickyHeader效果(筆記)
最近要做一個類似QQ瀏覽器的瀏覽記錄效果 就是下面的效果 QQ瀏覽器玩了半天,分析出了一點東西。 之前用ItemDecoration給RecyclerView繪製item過分割線。 大家仔細觀察一下就可以發現,這個效果就是給指定的item繪製分割線。 這裡有一個比較好的
Android RecyclerView 使用ItemDecoration 實現吸附效果
public class StickyItemDecoration extends RecyclerView.ItemDecoration { private ISticky mISticky; //矩形高度 private int mRectHeight; //文字TextSize private
Memcached集群之通過Repcached實現主從復制(待實踐)
能夠 htm .com 主從 hive sql ive ner 主從復制 暫時了解有這東西,不搭建了。 Mamcached可以通過Repcached實現主從復制。有以下優缺點: 優點: 1、能夠實現Cache的冗余功能 2、主從之間可以互相讀寫(亮點) 參考
EasyUI 通過 Combobox 實現 AutoComplete 效果
() -c com img ext ring 回復 lte 技術分享 朋友在做一個web程序,用的EasyUI框架,讓我幫忙實現一個自動提示功能。由於之前我也沒用過EasyUI框架,就想到了jQueryUI有 AutoComplete 插件,就想直接拿過來用。 但當我將jQ
Mycat 之 通過Keepalived 實現高可用
alived com -o ali vpd type gin blog process 一、系統拓撲圖 一、操作方法 參考本博客的Nginx + Keepalived 實現高可用Mycat 之 通過Keepalived 實現高可用
Spring入門之五-------SpringIoC之通過註解實現
string類型 protected abstract 準備工作 @service urn 解決辦法 sin val 一、準備工作 創建一個Class註解@Configuration,如下例子: @Configuration // 該註解可理解為將當前class等同於一個
React Native之通過createStackNavigator實現攜帶引數的頁面與頁面之間的跳轉
1 實現的功能 在網上看React Native文件,我特碼就想實現一個頁面到另外一個頁面的跳轉,然後另外一個頁面怎麼獲取引數,特麼沒找到一個說清楚的,要麼太複雜,要麼說了不理解,下面是我自己寫的一個App.js檔案,實現一個Home頁面跳到另外Details頁面,並且攜
(二)使用apache的commons-httpclient-3.1.jar之通過Java實現發起HTTP請求【使用代理】
本檔案介紹在需要通過代理才能訪問外網的情況下如何以純Java實現發起HTTP的請求。此部分內容與上一篇部落格內容大同小異,只有兩處稍有不同。 1.準備需要的Jar包 需要的jar包與上一篇介紹的相同,包括:commons-httpclient-3.
自定義控制元件三部曲檢視篇(五)——RecyclerView系列之二ItemDecoration
從來不跌倒不算光彩,每次跌倒後能再站起來,才是最大的榮耀。 一、新增分割線 1.1 引入ItemDecoration 在上一篇中,我們講解了RecyclerView的基本使用方法,但有個問題:為什麼Item之間沒有分割線呢?其實,給RecyclerView新
[2.2]Spark DataFrame操作(二)之通過反射實現RDD與DataFrame的轉換
參考 場景 檔案/home/pengyucheng/java/rdd2dfram.txt中有如下4條記錄: 1,hadoop,11 2,spark,7 3,flink,5 4,ivy,27 編碼實現:查詢並在控制檯打印出每行第三個欄位值大於7
android通過shape實現虛線效果
shape資源 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.androi
java:通過LocalDate實現日曆效果
看書《java核心技術I》提到:用來儲存時間的兩個類為Date和LocalDate。 LocalDate類採用的是時間和日曆分開,不需要使用構造器來構造一個LocalDate物件,而直接使用靜態工廠方法來構造一個物件。 下面就通過LocalDate類,寫出一個與日曆相當的程
Android探索之路:實現下拉重新整理功能
RefresrefreshableView = (RefreshableView) findViewById(R.id.refreshable_view);//找到相應的控制元件//下拉 refreshableView.setOnRefreshListener(new RefreshableV
Android探索之路:實現登入介面的記住密碼功能
目的功能: 點選 記住密碼時,下次登入時,輸入賬號,密碼自動出現 下面是實現的主要過程: 1、註冊時,除了填寫使用者的一些資訊,還需要在資料庫中設一個欄位,用於判斷使用者是否已經註冊 private
通過ScrollView實現滾動效果
此文,僅做為個人學習Android,記錄成長以及方便複習!通過ScrollView添加了滾動條,解決內容過長顯示不全,通過滾動顯示檢視!首先是用ScrollView把需要滾動顯示的內容包裹起來ScrollView 垂直滾動 HorizontalScrollView
翻翻git之---利用RecyclerView實現摺疊效果 SectionedExpandableGridRecyclerView
今天也沒有P1,因為年前醬油打多了,事情堆積到現在有點緊迫感了,快點給觀眾老爺上完聊我就去做事了!! 今天上的是一個可摺疊的RecyclerView SectionedExpandableGridRecyclerView(名字好長) 先上下效果圖:
RecyclerView利用ItemDecoration實現頭部懸停效果【類似微信通訊錄效果】
對於RecyclerView的ItemDecoration相信大家都不會陌生,因為RecyclerView並不像ListView那樣自帶分割線,所以我們需要繼承ItemDecoration去手動繪製分割線。本篇文章主要是利用ItemDecoration的特性來繪
RecyclerView.ItemDecoration實現頂部懸浮效果
StickyDecoration 專案地址:Gavin-ZYX/StickyDecoration 更多:作者 提 Bug 標籤: 利用RecyclerView.ItemDecorati
RecyclerView使用ItemDecoration實現吸頂效果
寫此部落格目的僅僅用於幫助自己做筆記 吸頂效果在很多APP都可以看見,現在可以藉助RecyclerView的ItemDecoration 來簡單實現一個吸頂效果直接上效果圖: 首先實現一個Model實體,該Model需要包含區分不同組的欄位,例如下面這