【鸿蒙开发实战篇】HarmonyOS 5.1手势事件详解
<p>大家好,我是 V 哥。<br>手势事件由绑定手势方法和绑定的手势组成,绑定的手势可以分为单一手势和组合手势两种类型,根据手势的复杂程度进行区分。本文跟着 V 哥一起来探讨手势事件处理。</p><p>想要考取鸿蒙认证的小伙伴,请加入V 哥班级获取辅导:</p>
<p>https://developer.huawei.com/consumer/cn/training/classDetail/042cb1cc4d7d44ecbdbd902fd1275dcc?type=1</p>
<p>V哥写的鸿蒙全系例图书,助你从入门到一名成熟的开发者:<br></p>
<p><br>## 一、绑定手势方法<br>通过给各个组件绑定不同的手势事件,并设计事件的响应方式,当手势识别成功时,ArkUI框架将通过事件回调通知组件手势识别的结果。</p>
<p>1. gesture(常规手势绑定方法),gesture为通用的一种手势绑定方法,可以将手势绑定到对应的组件上,方法格式如下所示:<br>```<br>.gesture(gesture: GestureType, mask?: GestureMask)<br>```<br>2. priorityGesture(带优先级的手势绑定方法),priorityGesture是带优先级的手势绑定方法,可以在组件上绑定优先识别的手势,方法格式如下所示:<br>```<br>.priorityGesture(gesture: GestureType, mask?: GestureMask)<br>```<br>在默认情况下,当父组件和子组件使用gesture绑定同类型的手势时,子组件优先识别通过gesture绑定的手势。当父组件使用priorityGesture绑定与子组件同类型的手势时,父组件优先识别通过priorityGesture绑定的手势。长按手势时,设置触发长按的最短时间小的组件会优先响应,会忽略priorityGesture设置。比如当父组件Column和子组件Text同时绑定TapGesture手势时,父组件以带优先级手势priorityGesture的形式进行绑定时,优先响应父组件绑定的TapGesture。示例代码如下所示:</p>
<p>**Demo0701.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0701 {<br>build() {<br> Column() {<br> Text('绑定手势事件-演示').fontSize(35)<br> // 通过gesture绑定TapGesture手势<br> .gesture(<br> TapGesture()<br> .onAction(() => {<br> console.info('文本组件的手势-触发中');<br> }))<br> }.width("100%").margin(10)<br> // 设置为priorityGesture时<br> // 点击文本区域会忽略Text组件的TapGesture手势事件<br> // 优先响应父组件Column的TapGesture手势事件<br> .priorityGesture(<br> TapGesture()<br> .onAction(() => {<br> console.info('容器的手势-触发中');<br> }),<br> // 忽略内部,仅响应当前组件的手势事件<br> GestureMask.IgnoreInternal<br> )<br>}<br>}<br>```<br>效果如下图所示:</p>
<p></p>
<p><br>3. parallelGesture(并行手势绑定方法),是并行的手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。<br>```<br>.parallelGesture(gesture: GestureType, mask?: GestureMask)<br>```<br>在默认情况下,手势事件为非冒泡事件,当父子组件绑定相同的手势时,父子组件绑定的手势事件会发生竞争,最多只有一个组件的手势事件能够获得响应。而当父组件绑定了并行手势parallelGesture时,父子组件相同的手势事件都可以触发,实现类似冒泡效果。</p>
<p>## 二、单一手势<br>1. 点击手势(TapGesture),点击手势支持单次点击和多次点击,拥有两个可选参数:count:声明该点击手势识别的连续点击次数。默认值为1,若设置小于1的非法值会被转化为默认值。如果配置多次点击,上一次抬起和下一次按下的超时时间为300毫秒。fingers:用于声明触发点击的手指数量,最小值为1,最大值为10,默认值为1。当配置多指时,若第一根手指按下300毫秒内未有足够的手指数按下则手势识别失败。方法格式如下所示:<br>```<br>TapGesture(value?:{count?:number, fingers?:number})<br>```<br>2. 长按手势(LongPressGesture),长按手势用于触发长按手势事件,拥有三个可选参数:fingers:用于声明触发长按手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。repeat:用于声明是否连续触发事件回调,默认值为false。duration:用于声明触发长按所需的最短时间,单位为毫秒,默认值为500。方法格式如下所示:<br>```<br>LongPressGesture(value?:{fingers?:number, repeat?:boolean, duration?:number})<br>```<br>以在Text组件上绑定可以重复触发的长按手势为例,演示长按手势,代码如下所示:</p>
<p>**Demo0702.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0702 {<br>//记录 手势的触发次数<br>@State count: number = 0;<br>build() {<br> Column() {<br> Text('长按手势-触发次数=' + this.count).fontSize(28)<br> .gesture(<br> // 绑定可以重复触发的LongPressGesture<br> LongPressGesture({ repeat: true })<br> .onAction((event: GestureEvent | undefined) => {<br> if (event) {<br> // 如果是可重复的长按手势,则count值一直递增<br> if (event.repeat) {<br> this.count++;<br> console.log("长按手势,触发次数=",this.count)<br> }<br> }<br> })<br> .onActionEnd(() => {<br> // 当长按手势结束的时候将count重置为0<br> console.log("长按手势,触发结束!")<br> })<br> ).width("90%")<br> }<br> .padding(10)<br> .border({ width: 3 })<br> .width("100%")<br>}<br>}<br>```</p>
<p>3. 拖动手势(PanGesture),拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:fingers:用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。方法格式如下所示:<br>```<br>PanGesture(value?:{ fingers?:number, direction?:PanDirection, distance?:number})<br>```<br>以在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动,实现代码如下所示:</p>
<p>**Demo0703.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0703 {<br>// 记录当前x轴偏移量<br>@State offsetX: number = 0;<br>// 记录当前y轴偏移量<br>@State offsetY: number = 0;<br>// 记录上一次x轴偏移量<br>@State positionX: number = 0;<br>// 记录上一次y轴偏移量<br>@State positionY: number = 0;<br>build() {<br> Column() {<br> Text('拖动手势,坐标:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)<br> .height(100).padding(10)<br> .fontColor(Color.White).backgroundColor(Color.Red)<br> .border({ width: 3 })// 在组件上绑定布局位置信息<br> .translate({ x: this.offsetX, y: this.offsetY, z: 0 })<br> .gesture(// 绑定拖动手势<br> PanGesture()<br> .onActionStart((event: GestureEvent | undefined) => {<br> console.info('开始拖动手势');<br> })// 当触发拖动手势时,根据回调函数修改组件的布局位置信息<br> .onActionUpdate((event: GestureEvent | undefined) => {<br> if (event) {<br> // 当前位置累加当前事件记录的偏移量得到横轴总偏移量<br> this.offsetX = this.positionX + event.offsetX;<br> // 当前位置累加当前事件记录的偏移量得到纵轴总偏移量<br> this.offsetY = this.positionY + event.offsetY;<br> }<br> }).onActionEnd(() => {<br> // 当手势结束的时候记录当前偏移量<br> this.positionX = this.offsetX;<br> // 当手势结束的时候记录当前偏移量<br> this.positionY = this.offsetY;<br> })<br> )<br> }.width("100%")<br>}<br>}<br>```<br>效果如下所示:</p>
<p></p>
<p>大部分可滑动组件,如List、Grid、Scroll、Tab等组件是通过PanGesture实现滑动,在组件内部的子组件绑定拖动手势(PanGesture)或者滑动手势(SwipeGesture)会导致手势竞争。</p>
<p>当在子组件绑定PanGesture时,在子组件区域进行滑动仅触发子组件的PanGesture。如果需要父组件响应,需要通过修改手势绑定方法或者子组件向父组件传递消息进行实现,或者通过修改父子组件的PanGesture参数distance使得拖动更灵敏。当子组件绑定SwipeGesture时,由于PanGesture和SwipeGesture触发条件不同,需要修改PanGesture和SwipeGesture的参数以达到所需效果。不合理的阈值设置会导致滑动不跟手(响应时延慢)的问题。<br>4.捏合手势(PinchGesture),捏合手势用于触发捏合手势事件,拥有两个可选参数:fingers:用于声明触发捏合手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。distance:用于声明触发捏合手势的最小距离,单位为vp,默认值为5,方法格式如下所示:<br>```<br>PinchGesture(value?:{fingers?:number, distance?:number})<br>```<br>我们可以在Column组件上绑定三指捏合手势,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大,代码如下所示:</p>
<p>**Demo0704.ets 示例**<br>```<br>@Entry<br>@Component<br>struct Index {<br> @State scaleValue: number = 1; <br> @State pinchValue: number = 1; <br> @State pinchX: number = 0; <br> @State pinchY: number = 0;<br> build() { <br> Column() { <br> Column() { <br> Text('PinchGesture scale:\n' + this.scaleValue) <br> Text('PinchGesture center:\n(' + this.pinchX + ',' + this.pinchY + ')') <br> } <br> .height(200) <br> .width(300) <br> .border({ width: 3 }) <br> .margin({ top: 100 }) <br> // 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大 <br> .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }) <br> .gesture( <br> // 在组件上绑定三指触发的捏合手势 <br> PinchGesture({ fingers: 3 }) <br> .onActionStart((event: GestureEvent|undefined) => { <br> console.info('Pinch start'); <br> }) <br> // 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例 <br> .onActionUpdate((event: GestureEvent|undefined) => { <br> if(event){ <br> this.scaleValue = this.pinchValue * event.scale; <br> this.pinchX = event.pinchCenterX; <br> this.pinchY = event.pinchCenterY; <br> } <br> }) <br> .onActionEnd(() => { <br> this.pinchValue = this.scaleValue; <br> console.info('Pinch end'); <br> }) <br> ) <br> } <br>}<br>}<br>```<br>5. 旋转手势(RotationGesture),旋转手势用于触发旋转手势事件,拥有两个可选参数:fingers:用于声明触发旋转手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。angle:用于声明触发旋转手势的最小改变度数,单位为deg,默认值为1。方法格式如下所示:<br>```<br>RotationGesture(value?:{fingers?:number, angle?:number})<br>```<br>我们可以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转,代码示例如下图所示:</p>
<p>**Demo0705.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0705 {<br>//记录组件需要旋转的角度<br>@State angle: number = 0;<br>//记录旋转手势的角度值<br>@State rotateValue: number = 0;<br>build() {<br> Column() {<br> Text('旋转手势演示-旋转角度-' + this.angle).<br> fontSize(20).width("80%").margin(20).<br> borderWidth(1).padding(10)<br> // 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转<br> .rotate({ angle: this.angle })<br> .gesture(<br> RotationGesture()<br> .onActionStart((event: GestureEvent|undefined) => {<br> console.info('开始旋转');<br> })<br> // 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度<br> .onActionUpdate((event: GestureEvent|undefined) => {<br> if(event){<br> this.angle = this.rotateValue + event.angle;<br> }<br> console.info('结束旋转');<br> })<br> // 当旋转结束抬手时,固定组件在旋转结束时的角度<br> .onActionEnd(() => {<br> this.rotateValue = this.angle;<br> console.info('获取旋转角度');<br> })<br> .onActionCancel(() => {<br> console.info('旋转取消');<br> })<br> )<br> }.width("100%")<br>}<br>}<br>```<br>效果如下图所示:</p>
<p></p>
<p><br>6. 滑动手势(SwipeGesture),滑动手势用于触发滑动事件,当滑动速度大于100vp/s时可以识别成功,拥有三个可选参数:fingers:用于声明触发滑动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。direction:用于声明触发滑动手势的方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为SwipeDirection.All。speed:用于声明触发滑动的最小滑动识别速度,单位为vp/s,默认值为100。方法格式如下所示:</p>
<p>```<br>SwipeGesture(value?:{fingers?:number, direction?:SwipeDirection, speed?:number})<br>```<br>我们可以在Column组件上绑定滑动手势实现组件的旋转为例,代码如下所示:</p>
<p>**Demo0706.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0706 {<br>//记录 旋转角度<br>@State rotateAngle: number = 0;<br>// 记录 滑动速度<br>@State speed: number = 1;<br>build() {<br> Column() {<br> Column() {<br> Text("滑动手势的速度:" + this.speed)<br> Text("滑动手势的角度:" + this.rotateAngle)<br> }<br> .border({ width: 3 })<br> .width("70%").padding(10).margin(20)<br> // 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度<br> .rotate({ angle: this.rotateAngle })<br> .gesture(<br> // 绑定滑动手势且限制仅在竖直方向滑动时触发<br> SwipeGesture({ direction: SwipeDirection.Vertical })<br> // 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改<br> .onAction((event: GestureEvent|undefined) => {<br> if(event){<br> this.speed = event.speed;<br> this.rotateAngle = event.angle;<br> }<br> })<br> )<br> }.width("100%")<br>}<br>}<br>```<br>效果如下所示:</p>
<p></p>
<p>当SwipeGesture和PanGesture同时绑定时,若二者是以默认方式或者互斥方式进行绑定时,会发生竞争。SwipeGesture的触发条件为滑动速度达到100vp/s,PanGesture的触发条件为滑动距离达到5vp,先达到触发条件的手势触发。可以通过修改SwipeGesture和PanGesture的参数以达到不同的效果。</p>
<p>## 三、组合手势<br>组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型。方法主要包括2个参数分别为:mode:为GestureMode枚举类。用于声明该组合手势的类型,取值顺序识别、并行识别和互斥识别三种类型。gesture:由多个手势组合而成的数组。用于声明组合成该组合手势的各个手势。方法具体格式如下所示:<br>```<br>GestureGroup(mode:GestureMode, gesture:GestureType[])<br>```<br>1. 顺序识别</p>
<p>顺序识别组合手势对应的GestureMode为Sequence。顺序识别组合手势将按照手势的注册顺序识别手势,直到所有的手势识别成功。当顺序识别组合手势中有一个手势识别失败时,后续手势识别均失败。顺序识别手势组仅有最后一个手势可以响应onActionEnd。<br>以一个由长按手势和拖动手势组合而成的连续手势为例,在一个Column组件上绑定了translate属性,通过修改该属性可以设置组件的位置移动。然后在该组件上绑定LongPressGesture和PanGesture组合而成的Sequence组合手势。当触发LongPressGesture时,更新显示的数字。当长按后进行拖动时,根据拖动手势的回调函数,实现组件的拖动。实现代码示例如下所示:</p>
<p>**Demo0707.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0707 {<br>//x坐标 偏移量<br>@State offsetX: number = 0;<br>//y坐标 偏移量<br>@State offsetY: number = 0;<br>//次数<br>@State count: number = 0;<br>//当前点的x坐标<br>@State positionX: number = 0;<br>//当前点的y坐标<br>@State positionY: number = 0;<br>//边框样式<br>@State borderStyles: BorderStyle = BorderStyle.Dotted</p>
<p>build() {<br> Column() {<br> Text('顺序手势识别\n' + '长按行为:' + this.count + <br> '\n拖拽手势:\nX: ' + this.offsetX + '\n' + 'Y: ' + <br> this.offsetY).fontSize(20).width("80%")<br> .margin(10).padding(10).borderWidth(2)<br> }<br> // 绑定translate属性可以实现组件的位置移动<br> .translate({ x: this.offsetX, y: this.offsetY, z: 0 })<br> .width("100%")<br> //以下组合手势为顺序识别,当长按手势事件未正常触发时不会触发拖动手势事件<br> .gesture(<br> // 声明该组合手势的类型为Sequence类型<br> GestureGroup(GestureMode.Sequence,<br> // 该组合手势第一个触发的手势为长按手势,且长按手势可多次响应 <br> LongPressGesture({ repeat: true })<br> // 当长按手势识别成功,增加Text组件上显示的count次数 <br> .onAction((event: GestureEvent|undefined) => {<br> if(event){<br> if (event.repeat) {<br> this.count++;<br> }<br> }<br> console.info('长按手势开始');<br> })<br> .onActionEnd(() => {<br> console.info('长按手势结束');<br> }),<br> // 当长按之后进行拖动,PanGesture手势被触发<br> PanGesture()<br> .onActionStart(() => {<br> this.borderStyles = BorderStyle.Dashed;<br> console.info('开始拖动');<br> })<br> //当该手势被触发时根据回调获得拖动的距离修改该组件的位移距离从而实现组件的移动 <br> .onActionUpdate((event: GestureEvent|undefined) => {<br> if(event){<br> this.offsetX = (this.positionX + event.offsetX);<br> this.offsetY = this.positionY + event.offsetY;<br> }<br> console.info('拖动已更新');<br> })<br> .onActionEnd(() => {<br> this.positionX = this.offsetX;<br> this.positionY = this.offsetY;<br> this.borderStyles = BorderStyle.Solid;<br> })<br> ).onCancel(() => {<br> console.log("顺序手势取消")<br> })<br> )<br>}<br>}<br>```<br>效果如下图所示:</p>
<p></p>
<p> </p>
<p>拖拽事件是一种典型的顺序识别组合手势事件,由长按手势事件和滑动手势事件组合而成。只有先长按达到长按手势事件预设置的时间后进行滑动才会触发拖拽事件。如果长按事件未达到或者长按后未进行滑动,拖拽事件均识别失败。</p>
<p>2. 并行识别<br>并行识别组合手势对应的GestureMode为Parallel。并行识别组合手势中注册的手势将同时进行识别,直到所有手势识别结束。并行识别手势组合中的手势进行识别时互不影响。<br>以在一个Column组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。实现代码如下所示:</p>
<p>**Demo0708.ets 示例**<br>```typescript<br>@Entry<br>@Component<br>struct Demo0708 {<br>//单击手势 次数<br>@State count1: number = 0;<br>//双击手势 次数<br>@State count2: number = 0;<br>build() {<br> Column() {<br> Text('并行识别手势\n' + '单击手势次数:' + this.count1 +<br> '\n双击手势次数:' + this.count2 )<br> .fontSize(20).padding(10).width("90%").borderWidth(2)<br> }.width('100%')<br> // 以下组合手势为并行并别,单击手势识别成功后<br> // 若在规定时间内再次点击,双击手势也会识别成功<br> .gesture(<br> GestureGroup(GestureMode.Parallel,<br> TapGesture({ count: 1 }).onAction(() => {this.count1++;}),<br> TapGesture({ count: 2 }).onAction(() => {this.count2++;})<br> )<br> )<br>}<br>}<br>```<br>效果如下图所示:</p>
<p></p>
<p></p>
<p><br>当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。当只有单次点击时,单击手势识别成功,双击手势识别失败。</p>
<p>当有两次点击时,若两次点击相距时间在规定时间内(默认规定时间为300毫秒),触发两次单击事件和一次双击事件。当有两次点击时,若两次点击相距时间超出规定时间,触发两次单击事件不触发双击事件。</p>
<p>3. 互斥识别</p>
<p>互斥识别组合手势对应的GestureMode为Exclusive。互斥识别组合手势中注册的手势将同时进行识别,若有一个手势识别成功,则结束手势识别,其他所有手势识别失败。</p>
<p>以在一个Column组件上绑定单击手势和双击手势组合而成的互斥识别组合手势为例。若先绑定单击手势后绑定双击手势,由于单击手势只需要一次点击即可触发而双击手势需要两次,每次的点击事件均被单击手势消费而不能积累成双击手势,所以双击手势无法触发。若先绑定双击手势后绑定单击手势,则触发双击手势不触发单击手势。代码实现如下所示:</p>
<p>**Demo0709.ets示例**<br>```typescript<br>@Entry<br>@Component<br>struct Index {<br>//记录 单击手势 的次数<br>@State count1: number = 0;<br>//记录 双击手势 的次数<br>@State count2: number = 0;<br>build() {<br> Column() {<br> Text('互斥识别手势\n' + '单击手势识别次数:' + this.count1 +<br> '\n双击手势识别次数:' + this.count2 + '\n')<br> .fontSize(20).borderWidth(2).padding(10).width("90%")<br> }<br> .width('100%')<br> //组合手势为互斥并别,单击手势识别成功后,双击手势会识别失败<br> .gesture(<br> GestureGroup(<br> GestureMode.Exclusive,<br> TapGesture({ count: 1 }).onAction(() => { this.count1++;}),<br> TapGesture({ count: 2 }).onAction(() => { this.count2++;})<br> )<br> )<br>}<br>}<br>```<br>效果如下图所示:</p>
<p></p>
<p></p>
<p><br>当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。当只有单次点击时,单击手势识别成功,双击手势识别失败。</p>
<p>当有两次点击时,手势响应取决于绑定手势的顺序。若先绑定单击手势后绑定双击手势,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。若先绑定双击手势后绑定单击手势,则会响应双击手势不响应单击手势。</p>
<p>## 四、多层级手势事件<br>多层级手势事件指父子组件嵌套时,父子组件均绑定了手势或事件。在该场景下,手势或者事件的响应受到多个因素的影响,相互之间发生传递和竞争,容易出现预期外的响应。</p>
<p>1. 触摸事件</p>
<p>触摸事件(onTouch事件)是所有手势组成的基础,有Down,Move,Up,Cancel四种。手势均由触摸事件组成,例如,点击为Down+Up,滑动为Down+一系列Move+Up。触摸事件具有最特殊性:<br>- 监听了onTouch事件的组件。若在手指落下时被触摸则均会收到onTouch事件的回调,被触摸受到触摸热区和触摸控制影响。<br>- onTouch事件的回调是闭环的。若一个组件收到了手指Id为0的Down事件,后续也会收到手指Id为0的Move事件和Up事件。<br>- onTouch事件的回调是一致的。若一个组件收到了手指Id为0的Down事件未收到手指Id为1的Down事件,则后续只会收到手指Id为0的touch事件,不会收到手指Id为1的后续touch事件。<br><br>对于一般的容器组件(例如:Column),父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件根据布局进行触发。<br>```<br>ComponentA() { <br> ComponentB().onTouch(() => {}) <br> ComponentC().onTouch(() => {})<br>}.onTouch(() => {})<br>```<br>组件B和组件C作为组件A的子组件,当触摸到组件B或者组件C时,组件A也会被触摸到。onTouch事件允许多个组件同时触发。因此,当触摸组件B时,会触发组件A和组件B的onTouch回调,不会触发组件C的onTouch回调。</p>
<p>当触摸组件C时,会触发组件A和组件C的onTouch回调,不触发组件B的回调。特殊的容器组件,如Stack等组件,由于子组件之间存在着堆叠关系,子组件的布局也互相存在遮盖关系。所以,父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件会存在遮盖关系。</p>
<p>2. 手势与事件</p>
<p>除了触摸事件(onTouch事件)外的所有手势与事件,均是通过基础手势或者组合手势实现的。比如拖拽事件是由长按手势和滑动手势组成的一个顺序手势。在未显式声明的情况下,同一时间,一根手指对应的手势组中只会有一个手势获得成功从而触发所设置的回调。因此,除非显式声明允许多个手势同时成功,同一时间只会有一个手势响应。响应优先级遵循以下条件:<br>- 当父子组件均绑定同一类手势时,子组件优先于父组件触发。<br>- 当一个组件绑定多个手势时,先达到手势触发条件的手势优先触发。<br>```<br>ComponentA() { <br> ComponentB()<br> .gesture(TapGesture({count: 1}))<br>}.gesture(TapGesture({count: 1})) <br>```<br>当父组件和子组件均绑定点击手势时,子组件的优先级高于父组件。因此,当在B组件上进行点击时,组件B所绑定的TapGesture的回调会被触发,而组件A所绑定的TapGesture的回调不会被触发。<br>```<br>ComponentA().gesture( <br> GestureGroup( <br> GestureMode.Exclusive, <br> TapGesture({count: 1}), <br> PanGesture({distance: 5}) <br>))<br>```<br>当组件A上绑定了由点击和滑动手势组成的互斥手势组时,先达到手势触发条件的手势触发对应的回调。若使用者做了一次点击操作,则响应点击对应的回调。若使用者进行了一次滑动操作并且滑动距离达到了阈值,则响应滑动对应的回调。</p>
<p>可以通过设置属性,控制默认的多层级手势事件竞争流程,更好的实现手势事件。目前,responseRegion属性和hitTestBehavior属性可以控制Touch事件的分发,从而可以影响到onTouch事件和手势的响应。而绑定手势方法属性可以控制手势的竞争从而影响手势的响应,但不能影响到onTouch事件。</p>
<p>3. responseRegion对手势和事件的控制</p>
<p>responseRegion属性可以实现组件的响应区域范围的变化。响应区域范围可以超出或者小于组件的布局范围。<br>```<br>ComponentA() { <br> ComponentB() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1})) <br> .responseRegion({Rect1, Rect2, Rect3})}.onTouch(() => {})<br> .gesture(TapGesture({count: 1}))<br> .responseRegion({Rect4})<br>```<br>当组件A绑定了.responseRegion({Rect4})的属性后,所有落在Rect4区域范围的触摸事件和手势可被组件A对应的回调响应。</p>
<p>当组件B绑定了.responseRegion({Rect1, Rect2, Rect3})的属性后,所有落在Rect1,Rect2和Rect3区域范围的触摸事件和手势可被组件B对应的回调响应。</p>
<p>当绑定了responseRegion后,手势与事件的响应区域范围将以所绑定的区域范围为准,而不是以布局区域为准,可能出现布局相关区域不响应手势与事件的情况。此外,responseRegion属性支持由多个Rect组成的数组作为入参,以支持更多开发需求。</p>
<p>4. hitTestBehavior对手势和事件的控制</p>
<p>hitTestBehavior属性可以实现在复杂的多层级场景下,一些组件能够响应手势和事件,而一些组件不能响应手势和事件。<br>```<br>ComponentA() { <br> ComponentB() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1}))<br> <br> ComponentC() { <br> ComponentD() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1})) <br> } <br>.onTouch(() => {}) <br> .gesture(TapGesture({count: 1})) <br> .hitTestBehavior(HitTestMode.Block)}<br> .onTouch(() => {})<br> .gesture(TapGesture({count: 1}))<br>```<br>HitTestMode.Block自身会响应触摸测试,阻塞子节点和兄弟节点的触摸测试,从而导致子节点和兄弟节点的onTouch事件和手势均无法触发。</p>
<p>当组件C未设置hitTestBehavior时,点击组件D区域,组件A、组件C和组件D的onTouch事件会触发,组件D的点击手势会触发。</p>
<p>当组件C设置了hitTestBehavior为HitTestMode.Block时,点击组件D区域,组件A和组件C的onTouch事件会触发,组件D的onTouch事件未触发。同时,由于组件D的点击手势因为被阻塞而无法触发,组件C的点击手势会触发。<br>```<br>Stack A() { <br> ComponentB() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1}))<br> <br> ComponentC() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1})) <br> .hitTestBehavior(HitTestMode.Transparent)<br>}.onTouch(() => {})<br>.gesture(TapGesture({count: 1}))<br>```<br>HitTestMode.Transparent自身响应触摸测试,不会阻塞兄弟节点的触摸测试。当组件C未设置hitTestBehavior时,点击组件B和组件C的重叠区域时,Stack A和组件C的onTouch事件会触发,组件C的点击事件会触发,组件B的onTouch事件和点击手势均不触发。</p>
<p>而当组件C设置hitTestBehavior为HitTestMode.Transparent时,点击组件B和组件C的重叠区域,组件A和组件C不受到影响与之前一致,组件A和组件C的onTouch事件会触发,组件C的点击手势会触发。而组件B因为组件C设置了HitTestMode.Transparent,组件B也收到了Touch事件,从而组件B的onTouch事件和点击手势触发。<br>```<br>ComponentA() { <br> ComponentB() <br> .onTouch(() => {}) <br> .gesture(TapGesture({count: 1}))<br>}.onTouch(() => {})<br>.gesture(TapGesture({count: 1}))<br>.hitTestBehavior(HitTestMode.None)<br>```<br>HitTestMode.None自身不响应触摸测试,不会阻塞子节点和兄弟节点的触摸控制。<br>当组件A未设置hitTestBehavior时,点击组件B区域时,组件A和组件B的onTouch事件均会触发,组件B的点击手势会触发。</p>
<p>当组件A设置hitTestBehavior为HitTestMode.None时,点击组件B区域时,组件B的onTouch事件触发,而组件A的onTouch事件无法触发,组件B的点击手势触发。<br>针对简单的场景,建议在单个组件上绑定hitTestBehavior。<br>针对复杂场景,建议在多个组件上绑定不同的hitTestBehavior来控制Touch事件的分发。</p>
<p>5. 绑定手势方法对手势的控制</p>
<p>设置绑定手势的方法可以实现在多层级场景下,当父组件与子组件绑定了相同的手势时,设置不同的绑定手势方法有不同的响应优先级。当父组件使用.gesture绑定手势,父子组件所绑定手势类型相同时,子组件优先于父组件响应。<br>```<br>ComponentA() { <br> ComponentB() <br> .gesture(TapGesture({count: 1}))<br>}.gesture(TapGesture({count: 1}))<br>```<br>当父子组件均正常绑定点击手势时,子组件优先于父组件响应。此时,单击组件B区域范围,组件B的点击手势会触发,组件A的点击手势不会触发。如果以带优先级的方式绑定手势,则可使得父组件所绑定手势的响应优先级高于子组件。<br>```<br>ComponentA() { <br> ComponentB() <br> .gesture(TapGesture({count: 1}))<br>}.priorityGesture(TapGesture({count: 1}))<br>```<br>当父组件以.priorityGesture的形式绑定手势时,父组件所绑定的手势优先级高于子组件。此时,单击组件B区域范围,组件A的点击手势会触发,组件B的点击手势不会触发。如果需要父子组件所绑定的手势不发生冲突,均可响应,则可以使用并行的方式在父组件绑定手势。<br>```<br>ComponentA() { <br> ComponentB() <br> .gesture(TapGesture({count: 1}))<br>}.parallelGesture(TapGesture({count: 1}))<br>```<br>当父组件以.parallelGesture的形式绑定手势时,父组件和子组件所绑定的手势均可触发。此时,单击组件B区域范围,组件A和组件B的点击手势均会触发。</p>
<p>## 小结<br>本文详细介绍了Harmony OS中的交互事件处理机制,涵盖了触屏事件、键鼠事件、焦点事件和拖拽事件等多种类型。首先,对Harmony OS中的事件分类进行了详细阐述,包括触屏事件、键鼠事件、焦点事件和拖拽事件,并解释了事件分发机制和手势事件的构成。接着,深入探讨了事件分发的具体过程,包括触摸测试和事件响应链的收集,以及如何通过触摸测试控制和自定义事件拦截来优化事件处理。</p>
<p>在触屏事件部分,详细讲解了点击事件、触摸事件等的触发条件和回调函数的使用方法,并通过示例代码展示了如何在实际应用中实现这些事件的处理。键鼠事件部分则介绍了鼠标事件和键盘事件的分类及其触发机制,包括鼠标悬浮、点击等事件的处理方法,以及如何通过键盘事件实现快捷键功能。</p>
<p>焦点事件的介绍包括焦点的基本概念、焦点链的形成和走焦规范,以及如何通过监听获焦和失焦事件来实现焦点的动态管理。此外,还介绍了如何设置组件的可获焦属性和焦点样式,以及如何使用FocusController实现主动获焦和失焦。</p>
<p>拖拽事件部分则详细介绍了拖拽操作的基本概念、触发方式和回调事件的使用,包括手势拖拽和鼠标拖拽的不同实现方式,以及如何通过设置拖拽背板图和使用UDMF实现数据的传递。</p>
<p>最后,手势事件的讲解涵盖了单一手势和组合手势的分类及其绑定方法,包括点击手势、长按手势、拖动手势、捏合手势、旋转手势和滑动手势等的使用场景和实现方式,并通过示例代码展示了如何在实际应用中实现复杂的手势交互。</p>
<p>通过本文的学习,读者可以全面掌握Harmony OS中各种交互事件的处理方法,提升应用的交互体验和用户满意度。</p>
</div>
<div id="MySignature" role="contentinfo">
<p>本文来自博客园,作者:威哥爱编程,转载请注明原文链接:https://www.cnblogs.com/finally-vince/p/19086947</p><br><br>
来源:https://www.cnblogs.com/finally-vince/p/19086947
頁:
[1]