注册

ConstraintLayout2.0一篇写不完之嵌套滚动怎么滚

在ConstraintLayout1.x阶段,它主要提供的能力是对静态布局的支撑,那么到2.x之后,MotionLayout的拓展,让它对动态布局的支持有了进一步的优化,在1.x阶段不能实现的嵌套滚动布局布局方式,现在也就非常简单了。

在没有ConstraintLayout的时候,要实现嵌套滚动布局,通常都是使用CoordinatorLayout来实现,但是这个东西的使用局限性比较大,能非常简单的实现的嵌套布局,就那么几种,如果要实现一些特别的滚动效果,就需要自定义behavior来实现,这样一来,嵌套滚动布局就成了一个比较复杂的布局方式了,而MotionLayout的出现,就可以完美的解决这样一个布局难题。

在ConstraintLayout2.x中,有两种方式来实现嵌套滚动布局。

CoordinatorLayout配合MotionLayout

这种方式实际上还是借助CoordinatorLayout,是一种比较早期的实现方案,如果是对CoordinatorLayout比较熟悉的开发者,可以很快改造现有代码来适配MotionLayout的嵌套滚动。

这种方案的布局结构如下:

CoordinatorLayout

--------AppBarLayout

----------------MotionLayout

--------NestedScrollView

可以发现,这种方式,实际上就是利用MotionLayout来替代之前在AppBarLayout里面的CollapsingToolbarLayout,借助MotionLayout来实现之前CollapsingToolbarLayout的一些折叠效果。

这种方式的一般套路结构如下。

image-20210223105619990

在AppBarLayout中,我们通过MotionLayout控制动画效果。

那么在这里,一般又有两个套路,一是直接使用MotionLayout,然后在代码里面通过AppBarLayout.OnOffsetChangedListener的回调,设置MotionLayout的progress,另一种是直接自定义MotionLayout,实现AppBarLayout.OnOffsetChangedListener,这样通用性比较强,示例如下。

class CollapsibleToolbar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {

override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
progress = -verticalOffset / appBarLayout?.totalScrollRange?.toFloat()!!
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
(parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
}
}

这两种方式没有本质上的不同,但是对于MotionEditor来说,如果使用自定义的MotionLayout,在非根布局下创建约束的时候会有一些问题(修改属性也会存在一些问题),所以,如果使用自定义MotionLayout的话,建议通过include的方式,引用新的根布局为自定义MotionLayout的方式来使用,而直接使用MotionLayout的方式,则没有这个限制,希望MotionEditor能早日改善这个问题。PS:好消息,Android Studio Arctic Fox已经修复了这个问题。

单纯MotionLayout实现

MotionLayout的出现,就是为了能替代动态的布局模型,所以,如果还使用CoordinatorLayout,那么就违背了开发者的初心了,所以,我们的目的就是去除CoordinatorLayout,而仅使用MotionLayout来实现嵌套滚动效果,实现滚动布局的大一统。

这种套路的一般结构如下所示。

MotionLayout

--------MotionLayout

--------NestedScrollView

我们可以发现,这里有两层MotionLayout,外层的MotionLayout,用于控制头部的伸缩布局,而内部的MotionLayout,则用于控制头部的滚动时效果。

这样一来,整个嵌套滚动的格局一下子就打开了,再也没了之前使用CoordinatorLayout的高度限制,效果限制,所有的内容,都可以通过约束来进行设置,再通过MotionLayout来进行动态约束,从而实现嵌套滚动布局。

对于外层的MotionLayout,它的Scene提供两个能力,一个是控制头部从200dp,变为56dp,即提供一个伸缩的功能,另一个重要的而且很容易被忽视的作用,就是给内层MotionLayout提供progress数据,有了这个progress,内部MotionLayout才能联动,这个和使用CoordinatorLayout配合MotionLayout使用要设置progress是一个道理。

我们来看下最外层的Scene,代码如下所示。

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@+id/start">

<OnSwipe
motion:dragDirection="dragUp"
motion:touchAnchorId="@+id/motionLayout"
motion:touchAnchorSide="bottom" />
</Transition>

<ConstraintSet android:id="@+id/start">
<ConstraintOverride
android:id="@id/motionLayout"
android:layout_height="200dp"
motion:motionProgress="0" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<ConstraintOverride
android:id="@id/motionLayout"
android:layout_height="56dp"
motion:motionProgress="1" />
</ConstraintSet>
</MotionScene>

对于非layout_constraintXXX_toXXXOf的约束,可以使用ConstraintOverride来直接覆写,这样可以少写很多重复的约束,这里的约束改变实际上只有两个,即layout_height从200变为56,而另一个重要的点,就是motionProgress的指定,motionProgress的作用就是设置motionProgress,如果不设置这个,那么progress数据是没办法传递到内部MotionLayout的,从而会导致内部无法联动。

解决完外部的MotionLayout之后,内部的MotionLayout就迎刃而解了,因为它真的就是一个平平常常的MotionLayout,你想要对它内部的元素做任何的改动,都和之前直接使用MotionLayout没有任何区别。

我们来看这个简单的例子,如图所示。

image-20210817161849272

头部伸缩配上文字的移动,一个简简单单的类CoordinatorLayout布局,外部的Scene我们已经解决了,再来看看内部的Scene,算了不看了,没什么必要,就是简单的体力劳动。

整个套路的布局结构如下所示。

image-20210817162156160

总体看来,MotionLayout是不是实现了大一统,它将滚动的布局效果,转化为了多层MotionLayout的Scene分解,利用progress串联起来,设计思路不可谓不精,一旦你熟练掌握了MotionLayout的各种基础布局,那么即使再复杂的布局,也能分而治之。

0 个评论

要回复文章请先登录注册