fix: 修复下拉手势无响应,改用自定义SwipeDownLayout

ViewPager2内部RecyclerView会拦截触摸事件,导致setOnTouchListener
和GestureDetector都收不到下拉手势。

改用SwipeDownLayout(自定义FrameLayout),在onInterceptTouchEvent中
检测下拉——事件到达子View之前就能观察到,原理同SwipeRefreshLayout。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-28 21:03:03 +09:30
parent 97a3054db2
commit 6021171a40
3 changed files with 89 additions and 37 deletions

View File

@@ -1,9 +1,7 @@
package com.xiaoqu.watch.ui.home package com.xiaoqu.watch.ui.home
import android.os.Bundle import android.os.Bundle
import android.view.GestureDetector
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
@@ -77,9 +75,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
private var debugTapCount = 0 private var debugTapCount = 0
private var lastTapTime = 0L private var lastTapTime = 0L
// ===== 下拉手势检测 =====
private lateinit var gestureDetector: GestureDetector
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding { override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
return FragmentHomeBinding.inflate(inflater, container, false) return FragmentHomeBinding.inflate(inflater, container, false)
} }
@@ -112,7 +107,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
initConfigPage() initConfigPage()
// 初始化下拉手势 // 初始化下拉手势
initGestureDetector() initSwipeDownDetector()
// 启动时钟定时器 // 启动时钟定时器
startClockUpdater() startClockUpdater()
@@ -150,9 +145,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
showRevokeConfirm() showRevokeConfirm()
} }
// 面板关闭时恢复 ViewPager2 滑动 // 面板关闭时恢复 ViewPager2 滑动和下拉检测
punchPanel.onDismiss = { punchPanel.onDismiss = {
binding.viewPager.isUserInputEnabled = true binding.viewPager.isUserInputEnabled = true
binding.swipeDownLayout.swipeEnabled = true
} }
} }
@@ -208,8 +204,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
/** 展开打卡面板 */ /** 展开打卡面板 */
private fun showPunchPanel() { private fun showPunchPanel() {
if (punchPanel.isShowing) return if (punchPanel.isShowing) return
// 禁用 ViewPager2 滑动 // 禁用 ViewPager2 滑动和下拉检测
binding.viewPager.isUserInputEnabled = false binding.viewPager.isUserInputEnabled = false
binding.swipeDownLayout.swipeEnabled = false
// 查询考勤状态 // 查询考勤状态
punchViewModel.fetchAttendance() punchViewModel.fetchAttendance()
// 展开面板 // 展开面板
@@ -218,33 +215,17 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// ===== 下拉手势 ===== // ===== 下拉手势 =====
/** 初始化下拉手势检测器 */ /**
private fun initGestureDetector() { * 初始化下拉手势检测
gestureDetector = GestureDetector(requireContext(), * 使用 SwipeDownLayout 在 onInterceptTouchEvent 中检测下拉
object : GestureDetector.SimpleOnGestureListener() { * 比 setOnTouchListener 更可靠——能在事件被子 View 消费前看到
override fun onFling( */
e1: MotionEvent?, private fun initSwipeDownDetector() {
e2: MotionEvent, binding.swipeDownLayout.onSwipeDown = {
velocityX: Float, // 只在主页page=1且面板未展开时响应
velocityY: Float if (binding.viewPager.currentItem == 1 && !punchPanel.isShowing) {
): Boolean {
// 只在主页page=1且面板未展开时响应下拉
if (binding.viewPager.currentItem != 1) return false
if (punchPanel.isShowing) return false
// 下拉velocityY > 0 且垂直速度大于水平速度
if (velocityY > 500 && Math.abs(velocityY) > Math.abs(velocityX)) {
showPunchPanel() showPunchPanel()
return true
} }
return false
}
})
// 在 ViewPager2 上设置触摸监听
binding.viewPager.getChildAt(0)?.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event)
false // 不消费事件,让 ViewPager2 正常处理
} }
} }

View File

@@ -0,0 +1,70 @@
package com.xiaoqu.watch.ui.widget
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import kotlin.math.abs
/**
* 支持下拉手势检测的 FrameLayout
* 在 onInterceptTouchEvent 中检测向下滑动,触发回调
* 不消费事件,子 View 的水平滑动(如 ViewPager2不受影响
*
* 原理同 SwipeRefreshLayout在事件分发给子 View 之前先观察,
* 只有明确的垂直下拉才拦截
*/
class SwipeDownLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object {
/** 最小下拉距离(像素) */
private const val SWIPE_THRESHOLD_PX = 80
/** 触摸容差:垂直距离必须大于水平距离的倍数 */
private const val DIRECTION_RATIO = 1.5f
}
/** 下拉触发回调 */
var onSwipeDown: (() -> Unit)? = null
/** 是否启用下拉检测(面板打开时禁用) */
var swipeEnabled = true
private var startX = 0f
private var startY = 0f
private var swiped = false
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (!swipeEnabled) return false
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
startX = ev.x
startY = ev.y
swiped = false
}
MotionEvent.ACTION_MOVE -> {
if (swiped) return false
val dy = ev.y - startY
val dx = ev.x - startX
// 向下滑动超过阈值,且垂直方向为主
if (dy > SWIPE_THRESHOLD_PX && abs(dy) > abs(dx) * DIRECTION_RATIO) {
swiped = true
onSwipeDown?.invoke()
// 不拦截事件return false让子 View 继续处理
// 这样 ViewPager2 的左右滑动不会被打断
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
swiped = false
}
}
// 始终返回 false不拦截事件只观察
return false
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 首页容器:固定状态栏 + ViewPager2 + 打卡面板覆盖层 --> <!-- 首页容器:固定状态栏 + ViewPager2 + 打卡面板覆盖层 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.xiaoqu.watch.ui.widget.SwipeDownLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipeDownLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/background"> android:background="@color/background">
@@ -43,4 +44,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </com.xiaoqu.watch.ui.widget.SwipeDownLayout>