RecyclerView實現吸底效果—ItemDecoration
這些天遇到一個列表數據吸底需求,如果不滿一屏就全部展示,如果超過一屏就讓底部懸浮在屏幕底部。
大概效果如下圖:
列表我們一般用RecyclerView來實現,關于底部懸浮這里有兩種實現方法,一種是通過測量RecyclerView內容高度,另一種是用我們熟悉的ItemDecoration來實現。
下面就具體介紹這兩種實現方式。
測量RecyclerView內容高度實現
這種方式很直觀,我們先獲取RecyclerView控件的高度h1,設置完數據后再獲取RecyclerView的內容高度h2,然后將h1與h2進行比較:
①如果h1大于等于h2,則說明內容沒有超出屏幕高度,此時只需要將數據完全展示即可。
②如果h1小于h2,則說明RecyclerView內容高度超出屏幕,此時RecyclerView可滾動,所以我們需要在RecyclerView底部顯示吸底的View。
原理示意圖
RecyclerView控件的高度我們定義為h1,如下圖所示:
通過recyclerView#getHeight方法獲取到的高度是固定的,就是布局文件中設定的recyclerView高度。
具體代碼為:
// 獲取RecyclerView控件高度
int recyclerViewHeight = recyclerView.getHeight();
LogUtils.e(TAG, "recyclerViewHeight: " + recyclerViewHeight);
RecyclerView內容的高度我們定義為h2,如下圖所示:
由上圖可知,h2的高度需要在RecyclerView繪制完成以后動態獲取,具體代碼如下所示:
// 獲取recyclerView的內容高度
int recyclerViewRealHeight = recyclerView.computeVerticalScrollRange();
LogUtils.e(TAG, "recyclerViewRealHeight: " + recyclerViewRealHeight);
h1>=h2的情況,具體如下圖所示:
我們只需要讓Recycler的Adapter普通Item布局和底部的Footer布局就可以了。
最后我們看下h1<h2的情況,具體如下圖所示:
我們在RecyclerView控件的上方,蓋一個布局,這個懸浮布局的實現要和Adapter中的Footer布局實現一樣。
具體實現方式
接著我們看下如何實現。具體分為如下幾個步驟:
①將RecyclerView的父布局修改為RelativeLayouot,在RelativeLayouot的底部、RecyclerView的上方添加一個Footer布局。
②讓Adapter支持兩種布局,普通Item和Footer布局
③在給RecyclerView設置完數據后,獲取RecyclerView的控件高度h1和RecyclerView的內容高度h2
④如果h1<h2,就讓RecyclerView上方的Footer布局顯示,否則就不顯示。
接下來看代碼:
①布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.view.RecyclerViewBottomFloatByViewHeightActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
<TextView
android:id="@+id/tv_bottom"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:background="#BCEAC1"
android:gravity="center"
android:text="我是底部"
android:visibility="gone" />
</RelativeLayout>
②關于RecyclerView.Adapter如何支持多種ViewType,這里就不再細說了,具體代碼實現文末有鏈接。
③獲取h1和h2的值:為了避免recyclerView獲取到的高度0,我們需要在給RecyclerView設置完數據之后,通過View#post(Runnable)方法獲取。具體代碼如下:
recyclerView.post(() -> {
// 獲取RecyclerView控件高度
int recyclerViewHeight = recyclerView.getHeight();
LogUtils.e(TAG, "recyclerViewHeight: " + recyclerViewHeight);
// 獲取recyclerView的內容高度
int recyclerViewRealHeight = recyclerView.computeVerticalScrollRange();
LogUtils.e(TAG, "recyclerViewRealHeight: " + recyclerViewRealHeight);
});
④默認情況下懸浮布局不顯示,只有h1<h2時,該懸浮布局才顯示,核心代碼如下:
// 根據剩余空間確定是否需要顯示吸底的圖表底部
if (recyclerViewHeight < recyclerViewRealHeight) {
tvBottom.setVisibility(View.VISIBLE);
} else {
tvBottom.setVisibility(View.GONE);
}
總結
需要說明的是,這種通過獲取View高度來實現單個View懸浮效果
的方式,不僅僅適用于RecyclerView,它更是一種通用的方式。但它的缺點也很明顯,需要根據不容的業務去計算不同的View的高度。
一般不推薦這種方式去實現,不過它可以當做一個保底方案,畢竟簡單粗暴易理解易實現。
ItemDecoration實現分組懸停原理
接下來我們來講解如何使用ItemDecoration來實現底部View懸浮效果。
我們知道,系統提供了DividerItemDecoration組件,讓我們方便的給RecyclerView繪制分割線。
DividerItemDecoration的具體使用方式請看RecyclerView設置分割線---DividerItemDecoration,具體代碼示例請看RecyclerViewDividerItemDecorationActivity。
這里簡單介紹下ItemDecoration。
接觸過ItemDecoration的同學知道,通過自定義ItemDecoration就可以實現酷炫的分組懸停效果。
ItemDecoration中有三個重要方法,源碼如下:
public static abstract class ItemDecoration {
...
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
這三個方法的作用如下:
ItemDecoration#getItemOffsets:通過Rect為每個Item設置偏移,為onDraw和onDrawOver方法中的繪制預留空間。
ItemDecoration#onDraw:通過該方法,在Canvas上繪制內容,在繪制Item之前調用。(如果沒有通過getItemOffsets設置偏移的話,Item的內容會將其覆蓋)
ItemDecoration#onDrawOver:通過該方法,在Canvas上繪制內容,在Item之后調用。(畫的內容會覆蓋在item的上層)
他們的層級關系如下圖所示:
需要說明的是,這三個方法都是針對每個可見Item的區域的,如果不加限制的話,每個Item都會調用它。
如果我們重寫了ItemDecoration#getItemOffsets
方法,該方法就會在現有Item空間的基礎上新增空間,所以這個操作也會修改我們RecyclerView內容高度。
具體實例請看RecyclerViewCustomItemDecorationDividerActivity和MyDividerItemDecoration。頁面打開方式如下所示:
在用ItemDecoration實現分組懸停的過程中,又可以細分為兩種方法。
一種是通過getItemOffsets方法預留空間,然后在onDrawOver中對應的區域繪制懸停的頭部。懸停的部分需要額外繪制,不會復用Adapter中的Item的View。
另一種方法是,將需要懸停的部分也繪制到Item中,Adapter中的Item是以組為基本單位,一個Item會包含組中的所有View,Item內部第一個元素就是需要繪制的懸停頭部。然后我們就可以在onDrawOver獲取第一個可見Item的頭部View,接著復用這個頭部View,將其繪制在頂部即可。
接下來對這兩種方式進行介紹。
分組懸停實現方式一:getItemOffsets預留空間,onDrawOver中重新繪制懸停View,不復用
先看下不添加ItemDecoration的效果:
再看下添加完ItemDecoration后的效果:
具體代碼請參照RecyclerViewCustomItemDecorationFloatGroupActivity。這個類中的實現其實是簡化了Gavin-ZYX/StickyDecoration項目中的實現。
這里需要說明的是,這種方法實現的核心是getItemOffsets預留空間,onDrawOver直接在Item上層繪制新的懸停布局,懸停布局不復用ItemView
。從上面的示例可以看出,分組的頭部View是在ItemDecoration中繪制的,在Adapter中不用繪制分組的頭部。
分組懸停實現方式二:onDrawOver中獲取Item中的可見View,從中獲取分組頭部View進行復用
這種方法,將需要懸停的部分也繪制到Item中,Adapter中的Item是一個組的所有元素,Item內部第一個元素就是需要繪制的懸停頭部。然后我們就可以在onDrawOver獲取第一個可見Item的頭部View,接著復用這個頭部View,將其繪制在頂部即可。
示意圖如下:
我們在onDrawOver中獲取到第一個可見子View,然后根據id從里面獲取到頭部View,接著將這個用canvas將這個View繪制出來即可。
有興趣的同學可以自行實現。
ItemDecoration實現吸底效果
我們的這個吸底效果跟分組懸停效果是有所不同的,分組懸停效果針對的是第一個可見的子View,吸底效果針對的是最后一個可見的子View。
我們的實現思路如下:
①讓RecyclerView.Adapter支持普通的Item和Footer類型的Item。
②通過ItemDecoration繪制懸停View。
emmmmm,看起來很簡單的樣子。
通過上面對ItemDecoration中三個核心方法的分析,這里我們選擇onDrawOver方法來完成繪制,直接在最后一個Item上方繪制一個一模一樣的Footer即可。
我們前面說過,onDrawOver這幾個方法是針對所有Item的,如果不加限制,則所有的Item都會繪制。
接下來就是選擇使用哪個可見子View
繪制這個Footer的問題了。我們有兩種選擇,一個是最后一個可見的子View——lastView
,一個是最后一個完全可見的子View——lastVisibleView
,他們的位置分別通過下面方法獲取到:
int lastPosition = ((LinearLayoutManager)recyclerView.getLayoutManager()).findLastVisibleItemPosition();
int lastCompletelyVisibleItemPosition = ((LinearLayoutManager)parent.getLayoutManager()).findLastCompletelyVisibleItemPosition();
關于RecyclerView常用方法的總結,請看RecyclerView常用方法總結。
在多數情況下,lastView跟lastVisibleView不是同一個,只有在最后一個可見View的底部剛好達到RecyclerView下邊界的時候,lastView跟lastVisibleView就是同一個了。
大多數情況下,lastView跟lastVisibleView都不是同一個,具體如下圖所示:
當某個Item的底部與RecyclerView的底部重疊時,lastView跟lastVisibleView就是同一個了,具體如下圖:
我們先看使用lastVisibleView來繪制底部懸浮View的情況。
lastVisibleView永遠在RecyclerView內部顯示,它的bottom的值會一直小于等于RecyclerView.getHeight的值的。
默認情況下,懸浮View會繪制在lastVisibleView內部,跟lastVisibleView底部對齊。所以我們需要給懸浮View設置一個向下的偏移量,這個偏移量的值就是RecyclerView.getHeight - lastVisibleView.getBottom的值。具體如下圖所示:
我們只需要給繪制好的Footer添加一個offset
的值,讓其向下偏移offset的值即可。
然而不幸的是,通過onDrawOver繪制的View,是不能超出Item下邊界范圍
的。如果超出對應Item的bottom區域的話就無法顯示,也就是說此路不通。
沒辦法了,只能看下lastView了。
我們以lastView.getTop的值-懸浮View高度
的結果作為繪制懸浮View的top值,所以懸浮View相當于一直懸浮在lastView的頂部。
幸運的是,即使超出Item上方區域,onDrawOver的內容也是正常顯示的。
接下來我們需要給top值設置一個偏移量,這個偏移量就是RecyclerView.getHeight - lastVisibleView.getTop的值。
具體如下圖所示:
最后我們看下效果:
具體實現請看RecyclerViewBottomFloatByItemDecorationActivity和BottomFloatItemDecoration。
github項目地址:Android_Base_Demo
RecyclerView相關的demo打開方式如下:
喜歡的話就點個贊吧!
參考
1、【Android】RecyclerView:打造懸浮效果