From 5fc7db8e83800344ada43022d3f9ff9538205ab0 Mon Sep 17 00:00:00 2001 From: piggyguy Date: Thu, 3 Jul 2025 09:15:27 +0800 Subject: [PATCH] Description: add gesture conflicts controll IssueNo: IC9IUH Feature or Bugfix: Feature Binary Source: No Signed-off-by: jiadexiang --- zh-cn/application-dev/ui/Readme-CN.md | 2 +- .../ui/arkts-common-events-drag-event.md | 14 +- .../ui/arkts-gesture-events-gesture-judge.md | 325 +++++++++++++++++- ...ction-development-guide-support-gesture.md | 9 +- 4 files changed, 328 insertions(+), 22 deletions(-) diff --git a/zh-cn/application-dev/ui/Readme-CN.md b/zh-cn/application-dev/ui/Readme-CN.md index 13210d67763..68bd6397bf2 100755 --- a/zh-cn/application-dev/ui/Readme-CN.md +++ b/zh-cn/application-dev/ui/Readme-CN.md @@ -185,7 +185,7 @@ - [单一手势](arkts-gesture-events-single-gesture.md) - [组合手势](arkts-gesture-events-combined-gestures.md) - [多层级手势事件](arkts-gesture-events-multi-level-gesture.md) - - [手势拦截](arkts-gesture-events-gesture-judge.md) + - [手势冲突处理](arkts-gesture-events-gesture-judge.md) - [支持统一拖拽](arkts-common-events-drag-event.md) - [支持焦点处理](arkts-common-events-focus-event.md) - 使用自定义能力 diff --git a/zh-cn/application-dev/ui/arkts-common-events-drag-event.md b/zh-cn/application-dev/ui/arkts-common-events-drag-event.md index 28c4b6aec4f..4252b3343de 100644 --- a/zh-cn/application-dev/ui/arkts-common-events-drag-event.md +++ b/zh-cn/application-dev/ui/arkts-common-events-drag-event.md @@ -1282,7 +1282,7 @@ Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 - } // 处理BEGIN状态 - handleBeginState(context: dragController.SpringLoadingContext): boolean { + handleBeginState(context: SpringLoadingContext): boolean { // 检查用户所拖拽的数据类型是否自己能够处理的 if (this.checkDataType(context?.dragInfos?.dataSummary)) { return true; @@ -1293,16 +1293,16 @@ Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 - } // Spring Loading处理入口 - handleSpringLoading(context: dragController.SpringLoadingContext) { + handleSpringLoading(context: SpringLoadingContext) { // BEGIN 状态时检查拖拽数据类型 - if (context.state == dragController?.DragSpringLoadingState.BEGIN) { + if (context.state == dragController.DragSpringLoadingState.BEGIN) { if (this.handleBeginState(context)) { // 我们已经在onDragEnter时刷新了提醒色,进入Spring Loading状态时,恢复UI,提醒用户继续保持不动 this.buttonBackgroundColor = this.normalColor; } return; } - if (context.state == dragController?.DragSpringLoadingState.UPDATE) { + if (context.state == dragController.DragSpringLoadingState.UPDATE) { // 奇数次UPDATE通知刷新提醒UI,偶数次复原UI if (context.currentNotifySequence % 2 != 0) { this.buttonBackgroundColor = this.reminderColor; @@ -1312,12 +1312,12 @@ Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 - return; } // 处理Spring Loading结束,触发视图切换 - if (context.state == dragController?.DragSpringLoadingState.END) { + if (context.state == dragController.DragSpringLoadingState.END) { this.isShowSheet = true; return; } // 处理CANCEL状态,复原UI - if (context.state == dragController?.DragSpringLoadingState.CANCEL) { + if (context.state == dragController.DragSpringLoadingState.CANCEL) { this.buttonBackgroundColor = this.normalColor; return; } @@ -1347,7 +1347,7 @@ Spring Loading的整个过程包含三个阶段:悬停检测 -> 回调通知 - // 当用户拖拽离开按钮范围,恢复UI this.buttonBackgroundColor = this.normalColor }) - .onDragSpringLoading((context: dragController.SpringLoadingContext)=>{ + .onDragSpringLoading((context: SpringLoadingContext)=>{ this.handleSpringLoading(context); }) }.width('100%').height('100%') diff --git a/zh-cn/application-dev/ui/arkts-gesture-events-gesture-judge.md b/zh-cn/application-dev/ui/arkts-gesture-events-gesture-judge.md index 19add58beb7..07f626a1a07 100644 --- a/zh-cn/application-dev/ui/arkts-gesture-events-gesture-judge.md +++ b/zh-cn/application-dev/ui/arkts-gesture-events-gesture-judge.md @@ -1,16 +1,21 @@ -# 手势拦截 +# 手势冲突处理 -手势拦截主要用于确保手势按需执行,有效解决手势冲突问题。典型应用场景包括:嵌套滚动、通过过滤组件响应手势的范围来优化交互体验。手势拦截主要采用[手势触发控制](#手势触发控制)和[手势响应控制](#手势响应控制)两种方式实现。 +手势冲突是指多个手势识别器在同一组件或重叠区域同时识别时产生竞争,导致识别结果不符合预期。常见冲突场景包括: +- 同一组件上的多手势(如按钮同时添加点击与长按手势)。 +- 父子组件的同类型手势识别器。 +- 系统默认手势与自定义手势(如scroll滑动手势与子组件点击手势冲突)。 -## 手势触发控制 +干预手势处理可有效解决冲突,除控制组件响应热区和命中测试模式外,主要通过以下三种方式:[自定义手势判定](#自定义手势判定)、[手势并行动态控制](#手势并行动态控制)、[阻止手势参与识别](#阻止手势参与识别)。 -手势触发控制是指在系统判定阈值已满足的条件下,应用可自行判断是否应拦截手势,使手势操作失败。 +## 自定义手势判定 -**图1** 手势触发控制流程图 +自定义手势判定是指在系统判定阈值已满足的条件下,应用可自行判断是否应拦截该手势,使该手势识别失败,从而将识别成功的机会留给其他手势。 + +**图1** 自定义手势判定流程图 ![gesture_judge](figures/gesture_judge.png) -手势触发控制涉及以下接口。 +自定义手势判定涉及以下接口。 | **接口** | **说明** | | ------- | -------------- | @@ -125,14 +130,14 @@ } ``` -## 手势响应控制 +## 手势并行动态控制 -手势响应控制指的是手势已经成功识别,但是开发者仍然可以通过调用API接口控制手势回调是否能够响应。 +手势并行动态控制指的是手势已经成功识别,但是开发者仍然可以通过调用API接口控制手势回调是否能够响应。 -**图3** 手势响应控制流程图 +**图3** 手势并行动态控制流程图 ![gesture_judge_controller](figures/gesture_judge_controller.png) -手势响应控制的前提是手势识别成功,如果手势不成功则不会产生手势回调响应。 +手势并行动态控制的前提是手势识别成功,如果手势不成功则不会产生手势回调响应。 1. 业务手势作业流:指真正触发UI变化的业务手势,比如使页面滚动的PanGesture,触发点击的TapGesture等。 @@ -142,7 +147,7 @@ 4. 动态开闭手势:指通过手势识别器的setEnable方法,控制手势是否响应用户回调。 -手势响应控制涉及以下接口。 +手势并行动态控制涉及以下接口。 | **接口** | **说明** | | ------- | -------------- | @@ -372,4 +377,300 @@ }.width('100%').height('100%').backgroundColor(0xDCDCDC) } } - ``` \ No newline at end of file + ``` + +## 阻止手势参与识别 + +手势识别基于[触摸测试](./arkts-interaction-basic-principles.md#触摸测试)的响应链结果进行,因此在用户按下时,通过控制响应链中手势识别器的参与状态,动态干预手势处理是高效的。 + +这需要结合[onTouchTestDone](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ontouchtestdone20)接口来实现: + +完成触摸测试后,系统通过该接口回调返回所有手势识别器对象。应用可根据类型、组件标识或关联组件信息筛选识别器,并通过调用[preventBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#preventbegin20)接口主动禁用特定识别器。 + +根据手势类型进行禁用: + +```typescript + .onTouchTestDone((event, recognizers) => { + for (let i = 0; i < recognizers.length; i++) { + let recognizer = recognizers[i]; + // 根据类型禁用所有滑动手势 + if (recognizer.getType() == GestureControl.GestureType.PAN_GESTURE) { + recognizer.preventBegin(); + } + } + }) +``` + +根据手势所归属的组件禁用: + +组件需要提前通过通用属性[id](../reference/apis-arkui/arkui-ts/ts-universal-attributes-component-id.md#id)配置组件标识。 + +```typescript + .onTouchTestDone((event, recognizers) => { + for (let i = 0; i < recognizers.length; i++) { + let recognizer = recognizers[i]; + // 禁用掉标识为myID的组件上的所有手势 + if (recognizer.getEventTargetInfo().getId() == "myID") { + recognizer.preventBegin(); + } + } + }) +``` + +根据是否系统内置手势禁用: + +```typescript + .onTouchTestDone((event, recognizers) => { + for (let i = 0; i < recognizers.length; i++) { + let recognizer = recognizers[i]; + // 禁用掉所有系统内置的手势 + if (recognizer.isBuiltIn()) { + recognizer.preventBegin(); + } + } + }) +``` + +根据具体情况组合使用这些条件。 + +> **说明:** +> +> 系统由内向外执行节点上的onTouchTestDone回调。 + +在NDK中onTouchTestDone与preventBegin对应的接口分别为[OH_ArkUI_SetTouchTestDoneCallback](../reference/apis-arkui/_ark_u_i___native_module.md#oh_arkui_settouchtestdonecallback)和[OH_ArkUI_PreventGestureRecognizerBegin](../reference/apis-arkui/_ark_u_i___native_module.md#oh_arkui_preventgesturerecognizerbegin),它们的使用方式及功能与ArkTS接口一致。 + +以下通过一个简化的视频播放界面交互为例来说明具体的用法: + +父容器(video_layer)绑定了多种手势: +- 点击:控制暂停/播放。 +- 双击:切换全屏。 +- 长按:快进。 +- 上下滑动:调节亮度。 +- 左右滑动:调整进度。 + +其内部下方的Slider组件(progress_layer)未绑定长按手势,导致用户长按Slider时会触发父容器的快进手势,不符合预期。 + +解决方案:在Slider上注册onTouchTestDone回调,通过该回调禁用非Slider组件的手势识别器,即可解决冲突。 + +以下为完整示例代码: + +```typescript +@Entry +@ComponentV2 +struct Index { + @Local progress: number = 496000 // 初始进度,秒 + @Local total: number = 27490000 // 总时长,秒 + @Local currentWidth: string = '100%' + @Local currentHeight: string = '100%' + private currentPosX: number = 0 + private currentPosY: number = 0 + private currentFullScreenState: boolean = true + private normalPlayTimer: number = -1; + private isPlaying: boolean = true; + private fastForwardTimer: number = -1; + + aboutToAppear(): void { + // 启动一个周期性定时器每隔一秒刷新一次进度 + this.startNormalPlayTimer() + } + + startNormalPlayTimer(): void { + if (this.normalPlayTimer != -1) { + this.stopNormalPlayTimer() + } + this.normalPlayTimer = setInterval(() => { + this.progress = this.progress + 1000 + }, 1000) + } + + stopNormalPlayTimer(): void { + if (this.normalPlayTimer == -1) { + return + } + clearInterval(this.normalPlayTimer) + this.normalPlayTimer = -1 + } + + startFastForwardTimer(): void { + if (this.fastForwardTimer != -1) { + this.stopFastForwardTimer() + } + this.fastForwardTimer = setInterval(() => { + this.progress = this.progress + 100000 + }, 100) + } + + stopFastForwardTimer(): void { + if (this.fastForwardTimer == -1) { + return + } + clearInterval(this.fastForwardTimer) + this.fastForwardTimer = -1 + } + + showMessage(message: string): void { + this.getUIContext().getPromptAction().showToast({ message: message, alignment: Alignment.Center }) + } + + resetPosInfo(): void { + this.currentPosX = 0 + this.currentPosY = 0 + } + + toggleFullScreenState(): void { + this.currentFullScreenState = !this.currentFullScreenState + if (this.currentFullScreenState) { + this.currentWidth = '100%' + this.currentHeight = '100%' + } else { + this.currentWidth = '100%' + this.currentHeight = '50%' + } + this.showMessage(this.currentFullScreenState ? '全屏播放' : '取消全屏播放') + } + + togglePlayAndPause(): void { + this.isPlaying = !this.isPlaying + if (!this.isPlaying) { + this.stopNormalPlayTimer() + } else { + // 重新启动 + this.startNormalPlayTimer() + } + this.showMessage(this.isPlaying ? '暂停播放' : '继续播放') + } + + doFastForward(start: boolean): void { + if (!start) { // 停止快进,恢复正常播放 + this.stopFastForwardTimer() + this.startNormalPlayTimer() + this.showMessage('取消快进') + return + } + + this.stopNormalPlayTimer() + this.startFastForwardTimer() + this.showMessage('开始快进') + } + + updateBrightness(start: boolean, event: BaseGestureEvent): void { + let newY = event.fingerList[0].localY + if (start) { + this.currentPosY = newY + this.showMessage('开始调整 亮度') + return + } + let offsetY = newY - this.currentPosY; + if (offsetY > 10) { + this.showMessage((offsetY > 0) ? '降低亮度' : '提高亮度') + } + this.currentPosY = newY + } + + updateProgress(start: boolean, event: BaseGestureEvent): void { + let newX = event.fingerList[0].localX + if (start) { + this.currentPosX = newX + this.showMessage('开始调整 进度') + return + } + let offsetX = newX - this.currentPosX; + this.progress = Math.floor(this.progress + offsetX * 10000) + this.currentPosX = newX + } + + build() { + Stack({ alignContent: Alignment.Center }) { + Column() { + Column() { + Text("播放进度:" + this.progress) + } + .width("100%").height("90%") + Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { + Slider({ + value: this.progress, + min: 0, + max: this.total, + style: SliderStyle.OutSet + }) + .onChange((value: number, mode: SliderChangeMode) => { + this.progress = value + }) + .id("progress_layer") + .onTouchTestDone((event, allRecognizers: Array) => { + for (let i = 0; i < allRecognizers.length; i++) { + let recognizer = allRecognizers[i]; + let inspectorInfo = recognizer.getEventTargetInfo().getId(); + if (inspectorInfo !== "progress_layer") { + // 但用户操作到进度条区域时,禁用掉所有非progress_layer上的手势,这样只会 + recognizer.preventBegin(); + } + } + }) + .margin({ left: 5 }) + .trackColor(Color.Red) + .blockColor(Color.Yellow) + .selectedColor(Color.Orange) + .trackThickness(2) + .flexShrink(1) + .flexGrow(1) + } + .flexGrow(1) + .flexShrink(1) + .id('id_progress_view') + } + } + .id('video_layer') + .backgroundColor('#E0E0E0') + .gesture( + GestureGroup(GestureMode.Exclusive, + PanGesture({ direction: PanDirection.Vertical, distance: 10 }) + .tag('pan_for_brightness_control') + .onActionStart((event) => { + this.updateBrightness(true, event) + }) + .onActionUpdate((event) => { + this.updateBrightness(false, event) + }), + PanGesture({ direction: PanDirection.Horizontal, distance: 10 }) + .tag('pan_for_play_progress_control') + .onActionStart((event) => { + this.updateProgress(true, event) + }) + .onActionUpdate((event) => { + this.updateProgress(false, event) + }), + + LongPressGesture() + .tag('long_press_for_fast_forward_control') + .onAction(() => { + this.doFastForward(true) // 开始快进 + }) + .onActionEnd(() => { + this.doFastForward(false) // 停止快进 + }) + .onActionCancel(() => { + this.doFastForward(false) + }), + + TapGesture({ count: 2 }) + .tag('double_tap_on_video') + .onAction(() => { + this.toggleFullScreenState() + }), + + TapGesture() + .tag('single_tap_on_video') + .onAction(() => { + this.togglePlayAndPause() + }) + ) + ) + .width(this.currentWidth) + .height(this.currentHeight) + } +} +``` + + + diff --git a/zh-cn/application-dev/ui/arkts-interaction-development-guide-support-gesture.md b/zh-cn/application-dev/ui/arkts-interaction-development-guide-support-gesture.md index dc8f84468c5..112d4b7c5b2 100644 --- a/zh-cn/application-dev/ui/arkts-interaction-development-guide-support-gesture.md +++ b/zh-cn/application-dev/ui/arkts-interaction-development-guide-support-gesture.md @@ -2,7 +2,7 @@ 当用户的操作符合某个手势的特征时,系统会将其识别为该手势,这一过程称为手势识别。为了响应某一个手势,需在组件上添加对应的手势对象,以便系统可以收集并进行处理。 -## 基本手势类型及特点 +## 基本手势及特点 | 手势 | 操作特征 | 触发方式举例 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | @@ -48,11 +48,16 @@ Pan A与 Pan B为不同阈值条件的滑动手势,Pan B为子组件上的, ## 干预手势处理 -系统对手势的识别仍然是以[触摸测试](./arkts-interaction-basic-principles.md#触摸测试)为前提,因此对于可用于干预基础事件处理的方式,都可以应用到对手势处理的干预上,除此之外还有以下方式可以使用: +手势框架基于上述原则处理各组件绑定的手势识别,但在许多场景中,应用需要动态控制手势识别过程:即通过干预手势处理逻辑,在满足响应规则的前提下实现预期识别结果。 + +系统对手势的识别仍然是以[触摸测试](./arkts-interaction-basic-principles.md#触摸测试)为前提,因此对于可用于干预基础事件处理的方式,都可以应用到对手势处理的干预上,除此之外还可以使用以下方式: | 方式 | 功能 | 对应API | 说明 | | ------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 自定义手势判定 | 在系统判定手势要成功时,给与应用对这个手势是否能成功的裁决机会 | [onGestureJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-customize-judge.md#ongesturejudgebegin) | 所绑定的组件上某个手势将要被系统判定为成功时,系统会回调该方法,给与应用机会来自主决定,该手势能不能成功,如果应用通过该方法返回拒绝,那么系统将会判定该手势失败,进而可以留给其他手势成功的机会。 | | 自定义手势判定增强 | 在系统判定手势要成功时,给与应用对这个手势是否能成功的裁决机会。 | [onGestureRecognizerJudgeBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ongesturerecognizerjudgebegin13) | 1. 同onGestureJudgeBegin,都是当所绑定的组件上某个手势将要被系统判定为成功时触发。
2. 该回调的优先级高于onGestureJudgeBegin,当绑定该方法时,onGestureJudgeBegin的绑定会失效。 | | 手势并行动态控制 | 控制父子(祖先与子孙)组件之间的PAN手势的联动关系,实现嵌套滚动。 | [shouldBuiltInRecognizerParallelWith](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#shouldbuiltinrecognizerparallelwith) | 1. 当用户按下,系统开始收集当前位置下所有需要参与手势处理的手势对象时触发。
2. 回调触发时,会将当前组件上的内置系统手势(当前只支持pan手势),以及系统在当前组件之前所已经收集到的同类型的手势对象(Pan手势),这些手势是一个数组,这些手势一般来自于子组件,因此其得到响应的优先级是比当前组件上的同类型手势要高的。普通情况下,当前组件上的手势是竞争不过子组件的手势的。
3. 而这个回调方法,就是给与应用机会,让应用可以强制指定自己身上的这个低优先级的同类别手势(pan手势)与子组件上的高优先级的同类别手势进行并行,也就是子组件上的同类别高优先级手势成功的时候,自己的也能被成功,从而得到响应。
4. 返回的手势对象中提供了控制手势响应使能的接口,因此在手势产生了并行之后,应用就可以自主控制两个并行手势的响应行为,从而达成嵌套滚动的效果(一个先滑,滑到底再触发另一个继续滑)。
**说明:**
在使用时,要注意所绑定的组件是具有系统内置手势(如list,swiper),否则使用该方法并没有作用,也没有意义。 | +| 阻止手势参与识别 | 在手势未识别之前,主动禁止某个手势参与本次交互识别。 | [preventBegin](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#preventbegin20) | 1. 当用户按下,系统收集完当前位置下所有能够参与本次处理的手势对象后触发组件上绑定的[onTouchTestDone](../reference/apis-arkui/arkui-ts/ts-gesture-blocking-enhancement.md#ontouchtestdone20)回调。
2. 回调触发时,会将按下时收集到的所有手势识别器对象都返回给开发者。
3. 应用可以在回调中根据每个手势识别器的信息,挑选出不希望哪些手势识别器参与本次处理,调用识别器对象上的preventBegin方法,从而避免手势冲突。 | + +更详细的使用指南请参考[手势冲突处理](arkts-gesture-events-gesture-judge.md)章节。 \ No newline at end of file -- Gitee