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

Motion Layout:可滑动视图的点击事件处理问题

问题

我正尝试为受Motion Layout控制的可滑动MaterialCardView设置触摸监听器,遇到如下问题:

  • 若在OnTouchListener的ACTION_DOWN事件中返回true(以接收手指抬起的回调),卡片的滑动动画将失效。
  • 若在ACTION_DOWN时执行业务逻辑,用户滑动卡片时该逻辑也会触发,我希望仅在点击卡片(非滑动操作)时才执行对应逻辑。

布局XML代码

<androidx.constraintlayout.motion.widget.MotionLayout 
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"
app:motionDebug="SHOW_ALL"
android:id="@+id/matchingMotionLayout"
android:layout_width="match_parent"
app:layoutDescription="@xml/matching_scene"
android:layout_height="match_parent">

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cardTwo"
    android:layout_width="320dp"
    android:layout_height="480dp"
    app:cardBackgroundColor="@color/design_default_color_secondary"
    app:cardCornerRadius="24dp"
    app:layout_constraintBottom_toTopOf="@id/dislikeButton"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/matchingWithTitle"
    app:layout_constraintVertical_bias="0.4">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/bottomImage"
            android:layout_height="0dp"
            android:layout_width="match_parent"
            android:scaleType="centerCrop"
            app:layout_constraintBottom_toTopOf="@id/bottomInterestPointName"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/bottomInterestPointName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:elevation="1dp"
            android:gravity="center_horizontal"
            android:paddingVertical="24dp"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Restaurant Name" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cardOne"
    android:layout_width="320dp"
    android:layout_height="480dp"
    app:layout_constraintBottom_toTopOf="@id/dislikeButton"
    app:cardBackgroundColor="@color/md_theme_light_primary"
    app:cardCornerRadius="24dp"
    app:layout_constraintTop_toBottomOf="@id/matchingWithTitle"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintVertical_bias="0.4">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/cardOneLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/topImage"
            android:layout_height="0dp"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            app:layout_constraintBottom_toTopOf="@id/topInterestPointName"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/topInterestPointName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:elevation="1dp"
            android:gravity="center_horizontal"
            android:paddingVertical="24dp"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Restaurant Name" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</com.jhn.restaurantpicker.ui.core.TestCardView>

...

</com.jhn.restaurantpicker.ui.core.TestMotionLayout>

Motion Scene过渡配置

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
app:defaultDuration="500"
xmlns:app="http://schemas.android.com/apk/res-auto">

<Transition
    android:id="@+id/startToLeft3"
    app:constraintSetEnd="@+id/unlike"
    app:constraintSetStart="@+id/start">

    <OnSwipe
        app:dragDirection="dragLeft"
        app:touchAnchorId="@id/cardOne"
        app:touchAnchorSide="left"
        app:touchRegionId="@id/cardOne" />

    <KeyFrameSet>

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/cardOne"
            app:percentY="0.3" />

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/topInterestPointName"
            app:percentY="0.3" />

    </KeyFrameSet>

</Transition>

<Transition
    android:id="@+id/startToRight3"
    app:constraintSetEnd="@+id/like"
    app:constraintSetStart="@+id/start">

    <OnSwipe
        app:dragDirection="dragRight"
        app:touchAnchorId="@id/cardOne"
        app:touchAnchorSide="right"
        app:touchRegionId="@id/cardOne" />

    <KeyFrameSet>

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/cardOne"
            app:percentY="-0.3" />

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/topInterestPointName"
            app:percentY="-0.3" />

    </KeyFrameSet>

</Transition>
...

Fragment过渡监听器代码

matchingMotionLayout.setTransitionListener(object : TransitionAdapter() {
            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                when (currentId) {
                    R.id.offScreenUnlike -> {
                        if (vm.shouldResetScene) {
                            motionLayout.progress = 0f
                            motionLayout.setTransition(R.id.start, R.id.start)
                        }
                        vm.unlike()
                    }

                    R.id.offScreenLike -> {
                        if (vm.shouldResetScene) {
                            motionLayout.progress = 0f
                            motionLayout.setTransition(R.id.start, R.id.start)
                        }
                        vm.like()
                    }

                    R.id.start -> {
                        motionLayout.progress = 0f
                        motionLayout.setTransition(R.id.start, R.id.start)
                    }
                }
                super.onTransitionCompleted(motionLayout, currentId)
            }
        })
解决方案

核心是通过判断手指移动距离区分点击和滑动操作,同时不拦截MotionLayout的触摸事件,保证滑动动画正常工作:

  1. 记录ACTION_DOWN时的触摸坐标,在ACTION_UP时计算移动距离,若小于设定阈值则判定为点击,执行业务逻辑。
  2. 所有触摸事件返回false,让MotionLayout能正常接收触摸事件处理滑动。

具体实现代码

private var startTouchX = 0f
private var startTouchY = 0f
// 滑动阈值,可根据需求调整,这里是8dp转成像素值
private val swipeThreshold = TypedValue.applyDimension(
    TypedValue.COMPLEX_UNIT_DIP,
    8f,
    resources.displayMetrics
).toInt()

cardOne.setOnTouchListener { _, event ->
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            // 记录初始触摸位置
            startTouchX = event.rawX
            startTouchY = event.rawY
            // 返回false,不拦截事件,让MotionLayout处理滑动
            false
        }
        MotionEvent.ACTION_UP -> {
            // 计算触摸移动的距离
            val moveX = abs(event.rawX - startTouchX)
            val moveY = abs(event.rawY - startTouchY)
            
            // 移动距离小于阈值,且MotionLayout未在执行过渡动画,判定为点击
            if (moveX < swipeThreshold && moveY < swipeThreshold && !matchingMotionLayout.isTransitioning) {
                // 执行你的点击业务逻辑
                vm.onCardClicked()
            }
            false
        }
        else -> false
    }
}

额外优化

可以结合MotionLayout的状态判断,避免在滑动动画过程中误触发点击逻辑:

  • matchingMotionLayout.isTransitioning 可以判断当前是否正在执行过渡动画,若为true则跳过点击逻辑。

内容的提问来源于stack exchange,提问作者Johan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 10:34:50