RecyclerView實現吸底效果—ItemDecoration

RecyclerView實現吸底效果—ItemDecoration

這些天遇到一個列表數據吸底需求,如果不滿一屏就全部展示,如果超過一屏就讓底部懸浮在屏幕底部。

大概效果如下圖:

image

列表我們一般用RecyclerView來實現,關于底部懸浮這里有兩種實現方法,一種是通過測量RecyclerView內容高度,另一種是用我們熟悉的ItemDecoration來實現。

下面就具體介紹這兩種實現方式。

測量RecyclerView內容高度實現

這種方式很直觀,我們先獲取RecyclerView控件的高度h1,設置完數據后再獲取RecyclerView的內容高度h2,然后將h1與h2進行比較:

①如果h1大于等于h2,則說明內容沒有超出屏幕高度,此時只需要將數據完全展示即可。

②如果h1小于h2,則說明RecyclerView內容高度超出屏幕,此時RecyclerView可滾動,所以我們需要在RecyclerView底部顯示吸底的View。

原理示意圖

RecyclerView控件的高度我們定義為h1,如下圖所示:

image

通過recyclerView#getHeight方法獲取到的高度是固定的,就是布局文件中設定的recyclerView高度。

具體代碼為:

// 獲取RecyclerView控件高度
int recyclerViewHeight = recyclerView.getHeight();
LogUtils.e(TAG, "recyclerViewHeight: " + recyclerViewHeight);

RecyclerView內容的高度我們定義為h2,如下圖所示:

image

由上圖可知,h2的高度需要在RecyclerView繪制完成以后動態獲取,具體代碼如下所示:

// 獲取recyclerView的內容高度
int recyclerViewRealHeight = recyclerView.computeVerticalScrollRange();
LogUtils.e(TAG, "recyclerViewRealHeight: " + recyclerViewRealHeight);

h1>=h2的情況,具體如下圖所示:

image

我們只需要讓Recycler的Adapter普通Item布局和底部的Footer布局就可以了。

最后我們看下h1<h2的情況,具體如下圖所示:

image

我們在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的上層)

他們的層級關系如下圖所示:

image

需要說明的是,這三個方法都是針對每個可見Item的區域的,如果不加限制的話,每個Item都會調用它。

如果我們重寫了ItemDecoration#getItemOffsets方法,該方法就會在現有Item空間的基礎上新增空間,所以這個操作也會修改我們RecyclerView內容高度。

具體實例請看RecyclerViewCustomItemDecorationDividerActivityMyDividerItemDecoration。頁面打開方式如下所示:

image

在用ItemDecoration實現分組懸停的過程中,又可以細分為兩種方法。

一種是通過getItemOffsets方法預留空間,然后在onDrawOver中對應的區域繪制懸停的頭部。懸停的部分需要額外繪制,不會復用Adapter中的Item的View。

另一種方法是,將需要懸停的部分也繪制到Item中,Adapter中的Item是以組為基本單位,一個Item會包含組中的所有View,Item內部第一個元素就是需要繪制的懸停頭部。然后我們就可以在onDrawOver獲取第一個可見Item的頭部View,接著復用這個頭部View,將其繪制在頂部即可。

接下來對這兩種方式進行介紹。

分組懸停實現方式一:getItemOffsets預留空間,onDrawOver中重新繪制懸停View,不復用

先看下不添加ItemDecoration的效果:

image

再看下添加完ItemDecoration后的效果:

image

具體代碼請參照RecyclerViewCustomItemDecorationFloatGroupActivity。這個類中的實現其實是簡化了Gavin-ZYX/StickyDecoration項目中的實現。

這里需要說明的是,這種方法實現的核心是getItemOffsets預留空間,onDrawOver直接在Item上層繪制新的懸停布局,懸停布局不復用ItemView。從上面的示例可以看出,分組的頭部View是在ItemDecoration中繪制的,在Adapter中不用繪制分組的頭部。

分組懸停實現方式二:onDrawOver中獲取Item中的可見View,從中獲取分組頭部View進行復用

這種方法,將需要懸停的部分也繪制到Item中,Adapter中的Item是一個組的所有元素,Item內部第一個元素就是需要繪制的懸停頭部。然后我們就可以在onDrawOver獲取第一個可見Item的頭部View,接著復用這個頭部View,將其繪制在頂部即可。

示意圖如下:

image

我們在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都不是同一個,具體如下圖所示:

image

當某個Item的底部與RecyclerView的底部重疊時,lastView跟lastVisibleView就是同一個了,具體如下圖:

image

我們先看使用lastVisibleView來繪制底部懸浮View的情況。
lastVisibleView永遠在RecyclerView內部顯示,它的bottom的值會一直小于等于RecyclerView.getHeight的值的。

默認情況下,懸浮View會繪制在lastVisibleView內部,跟lastVisibleView底部對齊。所以我們需要給懸浮View設置一個向下的偏移量,這個偏移量的值就是RecyclerView.getHeight - lastVisibleView.getBottom的值。具體如下圖所示:

image

我們只需要給繪制好的Footer添加一個offset的值,讓其向下偏移offset的值即可。

然而不幸的是,通過onDrawOver繪制的View,是不能超出Item下邊界范圍的。如果超出對應Item的bottom區域的話就無法顯示,也就是說此路不通。

沒辦法了,只能看下lastView了。

我們以lastView.getTop的值-懸浮View高度的結果作為繪制懸浮View的top值,所以懸浮View相當于一直懸浮在lastView的頂部。

幸運的是,即使超出Item上方區域,onDrawOver的內容也是正常顯示的。

接下來我們需要給top值設置一個偏移量,這個偏移量就是RecyclerView.getHeight - lastVisibleView.getTop的值。

具體如下圖所示:

image

最后我們看下效果:

image

具體實現請看RecyclerViewBottomFloatByItemDecorationActivityBottomFloatItemDecoration

github項目地址:Android_Base_Demo

RecyclerView相關的demo打開方式如下:

image

喜歡的話就點個贊吧!

參考

1、【Android】RecyclerView:打造懸浮效果

2、Gavin-ZYX/StickyDecoration

3、RecyclerView設置分割線---DividerItemDecoration

4、RecyclerView常用方法總結

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容