如何通过MotionLayout实现NestedScrollView滚至顶部后再展开Toolbar图片
问题背景
动画运行正常,但交互逻辑不符合需求:滚动垂直方向的NestedScrollView时,toolbar_image会立即展开。期望实现的逻辑是:先向下滚动让NestedScrollView到达内容顶部,再次下拉时toolbar_image才展开,而非立即触发。此前使用CoordinatorLayout搭配ObjectAnimator,通过在MotionEvent.ACTION_MOVE中检查!nestedScrollView.canScrollVertically(-1)实现了该效果,现在希望改用MotionLayout + MotionScene完成,当前MotionScene代码如下:
<?xml version="1.0" encoding="utf-8"?> <MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <Transition app:constraintSetEnd="@id/collapsed" app:constraintSetStart="@id/expanded"> <OnSwipe app:dragDirection="dragUp" app:touchAnchorId="@id/nestedScrollView" app:touchAnchorSide="top"/> <KeyFrameSet> <KeyAttribute app:framePosition="60" app:motionTarget="@id/toolbar_image"> <CustomAttribute app:attributeName="imageAlpha" app:customIntegerValue="255" /> </KeyAttribute> <KeyAttribute app:framePosition="90" app:motionTarget="@id/toolbar_image"> <CustomAttribute app:attributeName="imageAlpha" app:customIntegerValue="0" /> </KeyAttribute> <KeyPosition app:keyPositionType="pathRelative" app:framePosition="50" app:motionTarget="@id/title" app:percentX="0.9" /> </KeyFrameSet> </Transition> <ConstraintSet android:id="@+id/expanded"> <Constraint android:id="@id/toolbar_image" android:layout_height="200dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Constraint android:id="@id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginBottom="24dp" android:scaleX="1.0" android:scaleY="1.0" app:layout_constraintBottom_toBottomOf="@id/toolbar_image" app:layout_constraintStart_toStartOf="parent" /> </ConstraintSet> <ConstraintSet android:id="@+id/collapsed"> <Constraint android:id="@id/toolbar_image" android:layout_height="?attr/actionBarSize" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Constraint android:id="@id/title" android:layout_marginStart="20dp" android:layout_marginBottom="0dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleX="0.625" android:scaleY="0.625" app:layout_constraintBottom_toBottomOf="@id/toolbar_image" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/toolbar_image" /> </ConstraintSet> </MotionScene>
效果对比
- 当前效果:滚动NestedScrollView时,toolbar_image会随滚动立即展开/收起,无需等NestedScrollView到顶部。
- 期望效果:向下滚动时,优先让NestedScrollView滚动到内容顶部,继续下拉才触发toolbar_image展开;向上滚动时,先收起toolbar_image,再让NestedScrollView滚动内容。
解决方案
核心思路和之前用CoordinatorLayout时一致:只有当NestedScrollView滚动到顶部时,才允许MotionLayout响应下拉展开的触摸事件。可以通过两种方式实现:
方式1:代码监听滚动状态控制MotionLayout
在Activity/Fragment中给NestedScrollView添加滚动监听,判断是否到达顶部,动态开启/关闭MotionLayout的拖拽过渡:
val nestedScrollView = findViewById<NestedScrollView>(R.id.nestedScrollView) val motionLayout = findViewById<MotionLayout>(R.id.motionLayout) nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, _, scrollY, _, _ -> // 判断是否处于顶部:scrollY为0且无法继续向上滚动 val isAtTop = scrollY == 0 && !v.canScrollVertically(-1) // 仅在顶部时允许MotionLayout响应拖拽过渡 motionLayout.setEnableDragTransition(isAtTop) })
方式2:自定义MotionLayout拦截触摸事件
如果需要更灵活的触摸控制,可以自定义MotionLayout,在触摸拦截阶段判断NestedScrollView的状态:
class CustomMotionLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : MotionLayout(context, attrs, defStyleAttr) { private lateinit var nestedScrollView: NestedScrollView fun bindNestedScrollView(nsv: NestedScrollView) { nestedScrollView = nsv } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { // 只有NestedScrollView在顶部时,才拦截触摸事件交给MotionLayout处理 val isAtTop = nestedScrollView.scrollY == 0 && !nestedScrollView.canScrollVertically(-1) return if (isAtTop) { super.onInterceptTouchEvent(event) } else { // 不拦截,让NestedScrollView处理滚动 false } } }
布局中替换为自定义MotionLayout:
<com.yourpackage.CustomMotionLayout android:id="@+id/motionLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/your_motion_scene"> <!-- 内部控件:toolbar_image、title、nestedScrollView --> </com.yourpackage.CustomMotionLayout>
代码中绑定NestedScrollView:
val motionLayout = findViewById<CustomMotionLayout>(R.id.motionLayout) val nestedScrollView = findViewById<NestedScrollView>(R.id.nestedScrollView) motionLayout.bindNestedScrollView(nestedScrollView)
说明
两种方式都能实现需求,方式1更简洁,方式2适合需要自定义更多触摸逻辑的场景。本质都是把NestedScrollView的滚动状态和MotionLayout的过渡触发绑定,和之前CoordinatorLayout的逻辑对齐。
内容的提问来源于stack exchange,提问作者No Name

