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的触摸事件,保证滑动动画正常工作:
- 记录ACTION_DOWN时的触摸坐标,在ACTION_UP时计算移动距离,若小于设定阈值则判定为点击,执行业务逻辑。
- 所有触摸事件返回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
相关产品推荐
相关产品推荐

