做列表拖拽或者长按弹窗时,最容易遇到的情况是:明明加上了长按手势,但只要手指稍微滑动一点,长按就不触发,列表直接跟着滑了;或者反过来说,只要轻轻一划,系统的滑动响应被长按手势吃掉,列表滑不动。更让人头疼的是,如果在长按里面弹出一个自定义的拖拽浮层,由于手势冲突没处理好,两个节点之间甚至在屏幕上“打架”,谁都不受控。

这种体验断层的根源,在于 ArkUI 的手势系统底层有一套既定的竞争机制。系统为了防误触,默认会评估手势的意图。当你在同一个组件上同时绑定多种手势时,如果不显式声明它们之间的关系,系统就会按默认规则去猜,这就导致了要么长按失效,要么滑动卡死。

手势事件的默认竞争行为

默认情况下,系统优先响应滑动手势,因为滑动被视作用户想要快速浏览内容的核心意图。如果你把 LongPressGesture 和列表本身的默认滑动放在一起,当手指按下移动距离超过系统阈值时,长按的计时器就会被取消。

一个常见的错误写法是这样:

typescript

Row() {
  Text('长按我试试')
}
.onClick(() => {})
.gesture(
  LongPressGesture({ repeat: false, duration: 500 })
    .onAction(() => {
      // 弹出菜单或进入拖拽模式
    })
)

这段代码在只是稍微移动一点点手指时,长按根本不会触发。如果你用了 priorityGesture,长按的优先级虽然提高了,但用户的正常滑动操作又会被拦截。

明确使用并行与互斥的组合

要解决这个问题,不能只靠提高优先级,需要显式定义组合手势(GestureGroup)。对于长按和拖拽,最合适的配置是 GestureMode.Parallel(并行)或者通过时序控制。但在长按与列表滑动的冲突中,更靠谱的做法是将长按放进一个组合手势里,让系统知道它们是独立的意图。

typescript

.gesture(
  GestureGroup(GestureMode.Parallel,
    LongPressGesture({ duration: 500 })
      .onAction(() => {
        this.isDragging = true // 开启自定义拖拽态
      }),
    PanGesture({ direction: PanDirection.Vertical })
      .onActionStart(() => {
        // 只有在没进入长按状态时才允许滚动
      })
  )
)

并行模式下,两个手势各自独立判定。长按满 500ms 触发,滑动距离达标触发滑动。但这里有个细节:如果长按成功,我们就应该拦截外层的列表滚动。这就涉及到了状态控制。当 isDragging 为 true 时,我们要在 PanGesture 的处理回调里加入 return 逻辑,或者动态解绑外层列表的滚动属性,让列表的滚动控制权和拖拽浮层的移动控制权顺利交接。

拖拽平移的手势状态机

长按起来之后的平移,尽量不要用 LongPressGesture 的 onActionUpdate,因为长按手势本身并不负责移动距离的精准追踪。正确做法是在长按回调里置位状态,然后让 PanGesture 接管后续的移动。

typescript

private offsetX: number = 0
private offsetY: number = 0

PanGesture({ fingers: 1, distance: 5 })
  .onActionStart((event: GestureEvent) => {
    if (this.isDragging) {
      // 记录起点
      this.startX = event.fingerList[0].localX
      this.startY = event.fingerList[0].localY
    }
  })
  .onActionUpdate((event: GestureEvent) => {
    if (this.isDragging) {
      // 实时更新浮层位置
      this.offsetX = event.offsetX
      this.offsetY = event.offsetY
    }
  })
  .onActionEnd(() => {
    if (this.isDragging) {
      this.isDragging = false // 松手复位
      this.offsetX = 0
      this.offsetY = 0
    }
  })

在这套状态机里,LongPressGesture 负责发令枪(置位 isDragging),PanGesture 负责跑马拉松(计算位移)。每次移动结束或者被打断,必须把偏移量清零,并将状态复位。常见的错误是只处理了 onActionEnd,没有处理 onActionCancel。如果此时来了个系统通知,手势被强制打断,isDragging 会一直保持为 true,之后连列表都滑不动了。

手势打断的回调兜底

用户的每一次触控,系统并不会百分之百保证有始有终。当系统弹窗覆盖、接听电话、或者组件被销毁时,onActionEnd 是等不到的,只有 onActionCancel 会被触发。

typescript

.onActionCancel(() => {
  this.isDragging = false
  this.offsetX = 0
  this.offsetY = 0
})

把复位逻辑抽成一个私有方法,在 onActionEnd 和 onActionCancel 里同时调用。不要觉得这种情况少就不写,一旦在复杂页面上出现,用户反馈的必定是“界面卡死”。

开发警示与排查手段

排查手势冲突时,别靠猜。在每次手势回调开始和结束时打日志,记录当前的动作和触发类型。看日志是长按被提前取消,还是滑动根本没有响应。如果长按被吞,检查 GestureMode 是否配置了并行;如果是滑动卡顿,检查长按内部是否做了耗时的同步计算,阻塞了主线程。手势逻辑越复杂,越要把判定和执行拆开,让长按只管标记,滑动只管偏移,把最重的计算放到 TaskPool 里。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐