Android开发Android开发经验谈Android开发

RecyclerView 实现吸底效果,这个必须要会啊

2019-08-23  本文已影响15人  bfc7f634299a

这些天遇到一个列表数据吸底需求,如果不满一屏就全部展示,如果超过一屏就让底部悬浮在屏幕底部。

大概效果如下图:

列表我们一般用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,如下图所示:

image

由上图可知,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);
    }
}

这三个方法的作用如下:

他们的层级关系如下图所示:

需要说明的是,这三个方法都是针对每个可见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的值。

具体如下图所示:

github项目地址:https://github.com/tinyvampirepudge/Android_Base_Demo

RecyclerView相关的demo打开方式如下:

喜欢的话就点个赞吧!

Android开发资料+面试架构资料 免费分享 点击链接 即可领取

《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》

上一篇下一篇

猜你喜欢

热点阅读