RecyclerView使用指南(五)—— 實現吸頂效果
宣告:原創作品,轉載請註明出處: ofollow,noindex">https://www.jianshu.com/p/c197e3bf8329
前一篇文字我講解了ItemDecoration的使用方式,這篇文章預設大家已經讀過 RecyclerView使用指南(四)—— 使用ItemDecoration ,所以,不熟悉ItemDecoration的同學請先去看前一篇文章。
OK,我們先來看一下我們將要實現的效果:

一、實現帶有Section的樣式
我們先重寫getItemOffsets()方法,增加outRect的高度,然後重寫onDraw()方法,畫出一個rectangle。程式碼如下:
public class DemoItemDecoration extends RecyclerView.ItemDecoration { private int mSectionHeight = 80; private Paint mPaint; public DemoItemDecoration() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.BLUE); } @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); outRect.top = mSectionHeight; } @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDraw(c, parent, state); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); float sectionLeft = parent.getLeft(); float sectionTop = child.getTop() - mSectionHeight; float sectionRight = parent.getWidth(); float sectionBottom = child.getTop(); c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaint); } } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); } }
實現了這樣一個效果:

二、實現分組
剛剛實現的每一個Item都有section,這與實際需求時不符的,那麼我們要把資料進行分組,每一組的第一條Item上面才有section,這裡,為了讓ItemDecoration不與資料來源發生直接關係,我們新增一個GroupBean類來描述是否需要增加section。如下:
public class GroupBean { private int mGroupId; private int mGroupPosition; private boolean mIsFirst; private boolean mIsLast; public GroupBean(int groupId, int groupPosition, boolean isFirst, boolean isLast) { mGroupId = groupId; mGroupPosition = groupPosition; this.mIsFirst = isFirst; this.mIsLast = isLast; } public int getGroupId() { return mGroupId; } public int getGroupPosition() { return mGroupPosition; } public boolean isFirst() { return mIsFirst; } public boolean isLast() { return mIsLast; } }
然後改寫我們的getItemOffsets()方法和onDraw()方法,只有每一個分組的第一條Item才顯示section。程式碼如下:
public class DemoItemDecoration extends RecyclerView.ItemDecoration { private int mSectionHeight = 80; private Paint mPaintSection; private Paint mPaintText; private List<GroupBean> mGroupBeans; public DemoItemDecoration(List<GroupBean> groupBeans) { //資料 mGroupBeans = groupBeans; //畫筆 mPaintSection = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintSection.setColor(Color.BLUE); mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintText.setTextSize(60); mPaintText.setColor(Color.YELLOW); } @Override public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); int position = parent.getChildAdapterPosition(view); if (mGroupBeans.get(position).isFirst()) { outRect.top = mSectionHeight; } } @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDraw(c, parent, state); } @Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); GroupBean groupBean = mGroupBeans.get(position); if (mGroupBeans.get(position).isFirst()) { float sectionLeft = parent.getLeft(); float sectionTop = child.getTop() - mSectionHeight; float sectionRight = parent.getWidth(); float sectionBottom = child.getTop(); c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } } } }
最後,在Activity中進行ItemDecoration與GroupBean列表的繫結,以及ItemDecoration與RecyclerView的繫結:
private void initRv() { List<GroupBean> groupBeans = new ArrayList<>(); //根據RecyclerView的資料來源,設定需要增加section的item for (Data data : mList) { //這裡就是模擬一下,所以我取4的倍數增加section int i = mList.indexOf(data); int groupId = i / 4; int groupPosition = i % 4; GroupBean groupBean = null; //這裡是假資料嘛,4的倍數有section,那餘數是3的時候肯定是分組的最後一個啦 if (groupPosition == 0) { groupBean = new GroupBean(groupId, groupPosition, true, false); } if (groupPosition == 3) { groupBean = new GroupBean(groupId, groupPosition, false, true); } groupBeans.add(groupBean); } RecyclerView recyclerView = findViewById(R.id.rv); recyclerView.setAdapter(new SingleItemAdapter(mList)); recyclerView.addItemDecoration(new DemoItemDecoration(groupBeans)); }
我們來看下效果:

好,這樣,我們就實現了分組的效果,但是我們想要的吸頂效果,section是應該顯示到Item圖層的上方的,那麼我們使用onDraw()方法來實現,顯然是不合理的,既然如此,我們就將onDraw()方法中的內容剪下到onDrawOver()中好了~
三、實現section在列表頂部懸浮
實現吸頂效果,我們還需要做到讓我們的section在列表頂部懸浮,來分析一下邏輯:
- 每個分組的第一條資料需要有section
- 列表的最上方必須顯示一個section
現在來修改一下onDrawOver(),實現一下:
@Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); GroupBean groupBean = mGroupBeans.get(position); //所有分組的第一條資料有section if (groupBean.isFirst()) { float sectionLeft = parent.getLeft(); float sectionTop = child.getTop() - mSectionHeight; float sectionRight = parent.getWidth(); float sectionBottom = child.getTop(); c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } //列表的最上方顯示section資訊(這裡section是第一條顯示的條目所對應的groupId) LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); if (position == firstVisibleItemPosition) { float sectionLeft = parent.getLeft(); float sectionTop = parent.getTop(); float sectionRight = parent.getWidth(); float sectionBottom = parent.getTop() + mSectionHeight; c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } } }
來看下效果:

嗯,我們實現了一個吸頂效果,但是兩個section進行更替的特效顯得比較粗糙啊,我們想要的是下面的section將上面的section頂上去,OK,我們再進行優化一下。
四、優化section更替的特效
我們仔細觀察上面的效果圖,當下面的section向上移動的時候,上面的section沒有移動,所以,看起來下面的section直接覆蓋到了它的上面。
那麼上面的section應該在什麼時機進行移動呢?它的底邊應該是在該分組中最後一個Item的底部的上方,所以,我們更改,當section的底部低於“分組中最後一個Item”時,section整體上移,移動的距離就是section的高度與條目底部的差。
我們只需更改onDrawOver()方法的程式碼:
@Override public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { super.onDrawOver(c, parent, state); int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { View child = parent.getChildAt(i); int position = parent.getChildAdapterPosition(child); GroupBean groupBean = mGroupBeans.get(position); //所有分組的第一條資料有section if (groupBean.isFirst()) { float sectionLeft = parent.getLeft(); float sectionTop = child.getTop() - mSectionHeight; float sectionRight = parent.getWidth(); float sectionBottom = child.getTop(); c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } //列表的最上方顯示section資訊(這裡section是第一條顯示的條目所對應的groupId) LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager(); int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); if (position == firstVisibleItemPosition) { //如果是本組的最後一條,section的底部就不能低於這個條目的底部 if (groupBean.isLast()) { //當條目的底部已經高於section的時候,section應該隨著條目的底部往上移動 if (child.getBottom() < mSectionHeight) { float sectionLeft = parent.getLeft(); float sectionTop = parent.getTop() - (mSectionHeight - child.getBottom()); float sectionRight = parent.getWidth(); float sectionBottom = parent.getTop() + mSectionHeight - (mSectionHeight - child.getBottom()); c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } else { float sectionLeft = parent.getLeft(); float sectionTop = parent.getTop(); float sectionRight = parent.getWidth(); float sectionBottom = parent.getTop() + mSectionHeight; c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } } else { float sectionLeft = parent.getLeft(); float sectionTop = parent.getTop(); float sectionRight = parent.getWidth(); float sectionBottom = parent.getTop() + mSectionHeight; c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection); c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText); } } } }
在Activity中新增資料:
private void initRv() { List<GroupBean> groupBeans = new ArrayList<>(); //根據RecyclerView的資料來源,設定需要增加section的item for (Data data : mList) { //這裡就是模擬一下,所以我取4的倍數增加section int i = mList.indexOf(data); int groupId = i / 4; int groupPosition = i % 4; GroupBean groupBean; //這裡是假資料嘛,4的倍數有section,那餘數是3的時候肯定是分組的最後一個啦 if (groupPosition == 0) { groupBean = new GroupBean(groupId, groupPosition, true, false); } else if (groupPosition == 3) { groupBean = new GroupBean(groupId, groupPosition, false, true); } else { groupBean = new GroupBean(groupId, groupPosition, false, false); } groupBeans.add(groupBean); } RecyclerView recyclerView = findViewById(R.id.rv); recyclerView.setAdapter(new SingleItemAdapter(mList)); recyclerView.addItemDecoration(new DemoItemDecoration(groupBeans)); }
最後看一下效果:

總結
這篇文章我們實現了一個吸頂效果的特效,是屬於比較高階的用法了,關於ItemDecoration的用法也用它進行收尾了。另外,示例程式碼中的冗餘程式碼比較多,主要是為了看起來容易理解,請小朋友們在使用過程中合理地優化程式碼。
參考文獻
https://blog.csdn.net/briblue/article/details/70211942