RecyclerView实现吸底效果—ItemDecoration

简介: RecyclerView实现吸底效果—ItemDecoration

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:打造悬浮效果

2、Gavin-ZYX/StickyDecoration

3、RecyclerView设置分割线---DividerItemDecoration

4、RecyclerView常用方法总结

相关文章
|
5月前
|
存储 监控 Linux
VMware vSphere 9.1 发布 - 企业级工作负载平台
ESX 9.1 & vCenter Server 9.1 | vSphere 9.1
1138 1
VMware vSphere 9.1 发布 - 企业级工作负载平台
|
缓存 数据处理 Android开发
Android经典实战之Kotlin常用的 Flow 操作符
本文介绍 Kotlin 中 `Flow` 的多种实用操作符,包括转换、过滤、聚合等,通过简洁易懂的例子展示了每个操作符的功能,如 `map`、`filter` 和 `fold` 等,帮助开发者更好地理解和运用 `Flow` 来处理异步数据流。
843 4
|
自然语言处理 JavaScript API
【HarmonyOS Next之旅】HarmonyOS开发基础知识(一)
本文主要介绍了HarmonyOS应用开发中的基础知识、配置文件结构及其说明。内容涵盖用户应用程序的基本概念,包括应用形态(安装型与元服务)、包结构(APP Pack与HAP)、核心组件(Ability)以及相关文件(库文件、资源文件、配置文件等)。重点解析了`config.json`配置文件的组成与内部结构,如`app`(全局配置)、`deviceConfig`(设备配置)、`module`(模块配置)三大核心部分,详细说明了各属性的功能与约束。此外,还提供了具体示例,帮助开发者更好地理解和使用HarmonyOS的配置体系。适用于JS/ArkTS开发环境。
909 0
|
图形学
unity一个按钮实现开和关
在 Unity 中,通过一个布尔变量 `count` 来记录按钮的状态(开/关),并在点击事件中根据该变量的值执行不同操作。代码示例展示了两种方法:一种是通过计数器的奇偶性判断状态,另一种是直接取反布尔变量。每次点击后更新状态变量,从而实现按钮的开/关切换。
|
人工智能 自然语言处理 安全
创新不设限,灵码赋新能:通义灵码新功能深度评测
自从2023年通义灵码发布以来,这款基于阿里云通义大模型的AI编码助手迅速成为开发者心中的“明星产品”。它不仅为个人开发者提供强大支持,还帮助企业团队提升研发效率,推动软件开发行业的创新发展。本文将深入探讨通义灵码最新版本的三大新功能:@workspace、@terminal 和 #team docs,分享这些功能如何在实际工作中提高效率的具体案例。
|
SQL 存储 数据库
SQL语句给予用户权限:技巧、方法与最佳实践
在数据库管理中,为用户分配适当的权限是确保数据安全性和操作效率的关键步骤
1142 5
|
消息中间件 弹性计算 监控
【Serverless架构组成及优势适用场景】
Serverless的弹性伸缩、按需计费、无状态等特性使得开发者能够更加专注于业务逻辑,摆脱繁琐的服务器管理。它的优势在于灵活应对突发性工作负载、降低成本、提高开发效率,尤其在事件驱动、微服务、后端API等场景中表现出色。虽然Serverless仍然在不断发展,但其已经在云计算领域掀起了一场革命,成为当今应用开发的热门选择。随着技术的不断演进,我们有理由期待Serverless将继续推动应用开发的创新,为我们构建更加高效、可靠的应用提供更多可能。
911 0
|
安全 网络安全 数据安全/隐私保护
送你一张免费的通配符证书,解决你的所有问题
在数字化时代,网站安全至关重要。通配符证书作为一种能够覆盖多个子域名的SSL证书,如同“万能帽子”,保护数据安全传输。尤其适合有多子域名的网站,避免网络风险。免费的通配符证书不仅节省成本,且申请简便,安全性高,由权威机构颁发。通过JoySSL平台,注册账号并使用特定注册码,即可轻松获得免费证书,快速完成域名验证与安装部署,保障网站安全。
|
数据可视化 关系型数据库 MySQL
【DataEase】零代码数据可视化分析工具的安装部署保姆级教程
【DataEase】零代码数据可视化分析工具的安装部署保姆级教程
6697 2
【DataEase】零代码数据可视化分析工具的安装部署保姆级教程