【Jetpack Compose】使用MotionLayout实现折叠标题栏

3,007 阅读6分钟

可折叠标题栏在众多的App上面已经频繁运用了,并且原生的XML方式提供了现成的CollapsingToolbarLayout控件来帮助开发者实现可折叠标题栏,然而在Compose中,目前还没有现成的组件可以拿来即用,本文就通过MotionLayout+swipe来实现一个带吸顶功能的折叠标题栏。

在Compose使用MotionLayout,需要提前添加一下依赖:

implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha11")

目前最新版本为:1.1.0-alpha11,稳定版本还停留在:1.0.1

笔者在前面文章中已经介绍过如何在Compose中使用约束布局:👀Jetpack Compose - 约束布局ConstrainLayout

定义折叠状态

首先通过枚举类定义好折叠的状态,这里定义两种状态:折叠COLLAPSED和展开EXPANDED

定义标题栏位置

我们先看下标题栏折叠前和折叠后的两种效果图,折叠前左上角显示返回键,左下角是标题,整个标题栏背景是一张图片,折叠后背景图隐藏,返回键位置不变,标题则处于布局正中间。

根据上面两张效果图,我们就可以把MotionLayout的开始和结束约束集先定义完成,使用ConstrainSet定义布局的位置。

先定义起始约束集

ConstrainSet中先定义好背景图、返回按钮、标题文本和下方的内容组件的引用,然后根据位置定义好四个引用的约束条件

  • 背景图约束条件:宽度全屏,顶部和父局对齐
  • 返回按钮约束条件:左边和顶部都是和父布局对齐
  • 标题文本约束条件:底部和背景图底部对齐,左边和父布局对齐,分别设置16dp的边距
  • 内容组件约束条件:宽度全屏,顶部和标题底部对齐,设置顶部边距16dp

这样我们就把标题栏的起始位置约束集定义完了,下面我们接着定义结束位置的约束集

和起始位置的约束集一样,还是先定义好背景图、返回按钮、标题文本和下方的内容组件的引用,然后根据位置定义好四个引用的约束条件

  • 背景图约束条件:宽度全屏,高度最终定格56dp,顶部、左右和父布局对齐
  • 返回按钮约束条件:左边和顶部都是和父布局对齐,位置不变,约束条件也不变
  • 标题文本约束条件:左右和父布局对齐,这样可以实现水平居中的效果,然后顶部、底部和返回按钮对齐,这样标题文本和返回按钮就在同一水平线上
  • 内容组件约束条件:宽度全屏,顶部和背景图底部对齐

到这为止我们就把标题栏折叠前后的约束集都定义完成了,下面我们就可以直接把定义好的约束集传给MotionLayout对应的参数即可。

MotionLayout有四个必传的参数:

  • start为起始位置的约束集
  • end为结束位置的约束集
  • progress整个动画过程的进度,在这可以看作折叠过程的进度
  • content内容组件

startend就是我们上面定义好的约束集,progress从外层传入,这里暂时先不着急管,在内容组件中我们把标题文本、返回按键、标题背景和标题下方组件定义好。

这里需要注意的两点就是,每个组件的layoutId必须和约束集中的引用id保持一致;还有一点就是背景图片的透明度随着progress逐渐变得不可变,在折叠状态下背景图片是不显示的,只显示它的背景颜色。

手势处理

定义好折叠标题栏位置和组件之后,接下来就需要处理上滑和下滑的手势事件了。在开始编码之前,先理一下处理的思路。

  • 滑动手势可以通过swipeable修饰符来处理,它记录折叠的状态,也就是文章最开始的CollapsingState,默认为展开状态,并且它还可以处理吸顶的效果,吸顶的阈值定义为0.4
  • 嵌套滑动可以通过NestedScrollConnection来处理,上滑的过程中,先将滑动事件交给swipeable,让它先处理状态栏的折叠事件,等到状态栏完全折叠之后,再将滑动事件交给父布局处理底部内容组件的滑动。

下面我们来看具体代码实现

这一部分的代码可能有点长,但是在重点的地方都添加了注释方便大家理解。

先看下嵌套滑动的部分NestedScrollConnection,这里我们复写了三个方法:

  • onPreScroll用来劫持预滑动事件,在手指上下滑动的开始我们先处理下向上滑动事件,处理的逻辑就是上面介绍的,先将滑动事件交给swipeable,让它先处理状态栏的折叠事件,等到状态栏完全折叠之后,再将滑动事件交给父布局处理底部内容组件的滑动,向下滑动的事件则不劫持,直接交给父布局处理。
  • onPostScroll获取子布局的滑动事件,这里只需要处理垂直方向的滑动,交给swipeable即可。
  • onPostFling这个事件是处理手指松开的逻辑,因为想实现在上下滑过程中达到某一个阈值的时候标题栏是否自动折叠或者展开,就需要交给swipeable来判断,阈值会在下面swipeable修饰符中定义。

接下来再看下整个布局,整体布局外部采用Box组件,然后给它添加swipeable修饰符,定义好它的statethresholds,滑动的方向为Vertical垂直方向,这里的anchors需要注意下,当锚点在0位置是它是折叠状态,当它的锚点为最大高度时它是展开状态,这也很容易理解。并且还需要给Box添加nestedScroll修饰符让它处理嵌套滑动的事件。

最后还记得上面提到的progress进度值么?CollapsingHeader的进度值通过滑动的进度来判断,

当它进度朝着折叠状态移动的时候,progress就是swipingState.progress.fraction;反之就是1 - swipingState.progress.fractionfraction是一个[0 - 1]的区间范围,它代表着滑动事件fromto的一个比例,也就是整个滑动事件进度的比例。

到这为止我们就将一个折叠标题栏的脚手架给搭起来了,最后我们只需要调用它并传递对应的参数即可,我们来看下调用处的代码

这里标题栏的下方内容我们传入一个列表,列表的item传入最简单的文本组件,运行一下看看折叠标题栏的效果

GIF因为过大这里压缩了一下,稍微影响了一点清晰度,大家见谅,想要体验下效果的可以自己运行下代码实际感受下😆~

关于我

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢~