RecyclerView實現吸底效果—ItemDecoration

RecyclerView實現吸底效果—ItemDecoration

本文已受權微信公衆號「玉剛說」獨家原創發佈java

這些天遇到一個列表數據吸底需求,若是不滿一屏就所有展現,若是超過一屏就讓底部懸浮在屏幕底部。android

大概效果以下圖:git

列表咱們通常用RecyclerView來實現,關於底部懸浮這裏有兩種實現方法,一種是經過測量RecyclerView內容高度,另外一種是用咱們熟悉的ItemDecoration來實現。github

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

測量RecyclerView內容高度實現

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

①若是h1大於等於h2,則說明內容沒有超出屏幕高度,此時只須要將數據徹底展現便可。微信

②若是h1小於h2,則說明RecyclerView內容高度超出屏幕,此時RecyclerView可滾動,因此咱們須要在RecyclerView底部顯示吸底的View。app

原理示意圖

RecyclerView控件的高度咱們定義爲h1,以下圖所示:ide

經過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內容高度。

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

在用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的值。

具體以下圖所示:

最後咱們看下效果:

具體實現請看RecyclerViewBottomFloatByItemDecorationActivityBottomFloatItemDecoration

github項目地址:Android_Base_Demo

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

喜歡的話就點個贊吧!

參考

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

二、Gavin-ZYX/StickyDecoration

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

四、RecyclerView經常使用方法總結

相關文章
相關標籤/搜索