You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过MotionLayout实现NestedScrollView滚至顶部后再展开Toolbar图片

问题解决:MotionLayout实现NestedScrollView滚动到顶部后再展开Toolbar Image

问题背景

动画运行正常,但交互逻辑不符合需求:滚动垂直方向的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:20:56