(创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,还请三连支持一波哇ヾ(@^∇^@)ノ)
目录
交互事件概述
通用事件按照触发类型来分类,包括触屏事件、键鼠事件、焦点事件和拖拽事件。
触屏事件:手指或手写笔在触屏上的单指或单笔操作。
键鼠事件:包括外设鼠标或触控板的操作事件和外设键盘的按键事件。
- 鼠标事件是指通过连接和使用外设鼠标/触控板操作时所响应的事件。
- 按键事件是指通过连接和使用外设键盘操作时所响应的事件。
焦点事件:通过以上方式控制组件焦点的能力和响应的事件。
拖拽事件:由触屏事件和键鼠事件发起,包括手指/手写笔长按组件拖拽和鼠标拖拽。
事件分发:描述触控类事件(不包括按键,焦点)响应链的命中收集过程。
手势事件由绑定手势方法和绑定的手势组成,绑定的手势可以分为单一手势和组合手势两种类型,根据手势的复杂程度进行区分。
使用通用事件
事件分发
概述
ArkUI触控事件,根据输入源不同,主要划分为touch类与mouse类。
touch类的输入源包含:finger、pen
mouse类的输入源包含:mouse、touchpad、joystick
由这两类输入源可以触发如下事件:
touch mouse 触摸事件 触摸事件 点击事件 鼠标事件 拖拽事件 点击事件 手势事件 拖拽事件 手势事件 无论是touch类事件还是mouse类事件,在ArkUI框架上均由触摸测试发起,触摸测试直接决定了ArkUI事件响应链的生成及事件的分发。
触摸测试
如下是对触摸测试结果影响较大的几个因素:
TouchTest:触摸测试入口方法,此方法无外部接口
hitTestBehavior:触摸测试控制
interceptTouch:事件自定义拦截
responseRegion:触摸热区设置
enabled:禁用控制
安全组件
其他属性设置:透明度/组件下线
TouchTest
TouchTest的触发时机由每次点按的按下动作发起,默认由组件树的根节点TouchTest方法作为入口。
hitTestBehavior可以由InterceptTouch事件变更。
触摸热区/禁用控制/透明度等不满足组件事件交互诉求,会导致立即返回父节点。
触摸测试控制
具体用法参考触摸测试控制
命中:触摸测试成功收集到当前组件/子组件的事件。
子组件对父组件触摸测试的影响,取决于最后一个没有被阻塞触摸测试的子组件。
HitTestMode.Default:默认不配hitTestBehavior属性的效果,自身如果命中会阻塞兄弟,但是不阻塞孩子。
HitTestMode.None:自身不接收事件,但不会阻塞兄弟/孩子继续做触摸测试。
HitTestMode.Block:阻塞孩子的触摸测试,如果自身触摸测试命中,会阻塞兄弟及父亲的触摸测试。
HitTestMode.Transparent:自身进行触摸测试,同时不阻塞兄弟及父亲。
自定义事件拦截
自定义事件拦截在按下触发时,可以根据业务状态动态改变组件的hitTestBehavior属性。
禁用控制
设置禁用控制的组件,包括其子组件不会发起触摸测试过程,会直接返回父节点继续触摸测试。
触摸热区设置
触摸热区设置会影响触屏/鼠标类的触摸测试,如果设置为0,或不可触控区域,则事件直接返回父节点继续触摸测试。
安全组件
ArkUI包含的安全组件有:使用位置组件、使用粘贴组件、使用保存组件等。
安全组件当前对触摸测试影响:如果有组件z序比安全组件靠前,且遮盖安全组件,则安全组件事件直接返回到父节点继续触摸测试。
事件响应链的收集
ArkUI事件响应链收集,根据右子树(按组件布局的先后层级)优先的后序遍历流程,流程为:
foreach(item=>(node.rbegin(),node.rend(){ item.TouchTest() } node.collectEvent()
响应链收集举例,按下图的组件树,hitTestBehavior属性均为默认,用户点按的动作如果发生在组件5上,则最终收集到的响应链,以及先后关系是5,3,1。
因为组件3的hitTestBehavior属性为Default,收集到事件后会阻塞兄弟节点,所以没有收集组件1的左子树。
触屏事件
触屏事件指当手指/手写笔在组件上按下、滑动、抬起时触发的回调事件。包括点击事件、拖拽事件和触摸事件。
图1 触摸事件原理
点击事件
点击事件是指通过手指或手写笔做出一次完整的按下和抬起动作。当发生点击事件时,会触发以下回调函数:
onClick(event: (event?: ClickEvent) => void)
vent参数提供点击事件相对于窗口或组件的坐标位置,以及发生点击的事件源。
例如通过按钮的点击事件控制图片的显示和隐藏。
@Entry @Component struct IfElseTransition { @State flag: boolean = true; @State btnMsg: string = 'show'; build() { Column() { Button(this.btnMsg).width(80).height(30).margin(30) .onClick(() => { if (this.flag) { this.btnMsg = 'hide'; } else { this.btnMsg = 'show'; } // 点击Button控制Image的显示和消失 this.flag = !this.flag; }) if (this.flag) { Image($r('app.media.icon')).width(200).height(200) } }.height('100%').width('100%') } }
图2 通过按钮的点击事件控制图片的显示和隐藏
触摸事件
当手指或手写笔在组件上触碰时,会触发不同动作所对应的事件响应,包括按下(Down)、滑动(Move)、抬起(Up)事件:
onTouch(event: (event?: TouchEvent) => void)
event.type为TouchType.Down:表示手指按下。
event.type为TouchType.Up:表示手指抬起。
event.type为TouchType.Move:表示手指按住移动。
event.type为TouchType.Cancel:表示打断取消当前手指操作。
触摸事件可以同时多指触发,通过event参数可获取触发的手指位置、手指唯一标志、当前发生变化的手指和输入的设备源等信息。
// xxx.ets @Entry @Component struct TouchExample { @State text: string = ''; @State eventType: string = ''; build() { Column() { Button('Touch').height(40).width(100) .onTouch((event?: TouchEvent) => { if(event){ if (event.type === TouchType.Down) { this.eventType = 'Down'; } if (event.type === TouchType.Up) { this.eventType = 'Up'; } if (event.type === TouchType.Move) { this.eventType = 'Move'; } this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: ' + event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:(' + event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:' + event.target.area.width + '\nheight:' + event.target.area.height } }) Button('Touch').height(50).width(200).margin(20) .onTouch((event?: TouchEvent) => { if(event){ if (event.type === TouchType.Down) { this.eventType = 'Down'; } if (event.type === TouchType.Up) { this.eventType = 'Up'; } if (event.type === TouchType.Move) { this.eventType = 'Move'; } this.text = 'TouchType:' + this.eventType + '\nDistance between touch point and touch element:\nx: ' + event.touches[0].x + '\n' + 'y: ' + event.touches[0].y + '\nComponent globalPos:(' + event.target.area.globalPosition.x + ',' + event.target.area.globalPosition.y + ')\nwidth:' + event.target.area.width + '\nheight:' + event.target.area.height } }) Text(this.text) }.width('100%').padding(30) } }
键鼠事件
键鼠事件指键盘,鼠标外接设备的输入事件。
鼠标事件
支持的鼠标事件包含通过外设鼠标、触控板触发的事件。
鼠标事件可触发以下回调:
名称 描述 onHover(event: (isHover: boolean) => void) 鼠标进入或退出组件时触发该回调。
isHover:表示鼠标是否悬浮在组件上,鼠标进入时为true, 退出时为false。
onMouse(event: (event?: MouseEvent) => void) 当前组件被鼠标按键点击时或者鼠标在组件上悬浮移动时,触发该回调,event返回值包含触发事件时的时间戳、鼠标按键、动作、鼠标位置在整个屏幕上的坐标和相对于当前组件的坐标。 当组件绑定onHover回调时,可以通过hoverEffect属性设置该组件的鼠标悬浮态显示效果。
图1 鼠标事件数据流
鼠标事件传递到ArkUI之后,会先判断鼠标事件是否是左键的按下/抬起/移动,然后做出不同响应:
是:鼠标事件先转换成相同位置的触摸事件,执行触摸事件的碰撞测试、手势判断和回调响应。接着去执行鼠标事件的碰撞测试和回调响应。
否:事件仅用于执行鼠标事件的碰撞测试和回调响应。
onHover
onHover(event: (isHover: boolean) => void)
鼠标悬浮事件回调。参数isHover类型为boolean,表示鼠标进入组件或离开组件。该事件不支持自定义冒泡设置,默认父子冒泡。
若组件绑定了该接口,当鼠标指针从组件外部进入到该组件的瞬间会触发事件回调,参数isHover等于true;鼠标指针离开组件的瞬间也会触发该事件回调,参数isHover等于false。
// xxx.ets @Entry @Component struct MouseExample { @State hoverText: string = 'Not Hover'; @State Color: Color = Color.Gray; build() { Column() { Button(this.hoverText) .width(200).height(100) .backgroundColor(this.Color) .onHover((isHover?: boolean) => { // 使用onHover接口监听鼠标是否悬浮在Button组件上 if (isHover) { this.hoverText = 'Hovered!'; this.Color = Color.Green; } else { this.hoverText = 'Not Hover'; this.Color = Color.Gray; } }) }.width('100%').height('100%').justifyContent(FlexAlign.Center) } }
该示例创建了一个Button组件,初始背景色为灰色,内容为“Not Hover”。示例中的Button组件绑定了onHover回调,在该回调中将this.isHovered变量置为回调参数:isHover。
当鼠标从Button外移动到Button内的瞬间,回调响应,isHover值等于true,isHovered的值变为true,将组件的背景色改成Color.Green,内容变为“Hovered!”。
当鼠标从Button内移动到Button外的瞬间,回调响应,isHover值等于false,又将组件变成了初始的样式。
onMouse
onMouse(event: (event?: MouseEvent) => void)
鼠标事件回调。绑定该API的组件每当鼠标指针在该组件内产生行为(MouseAction)时,触发事件回调,参数为MouseEvent对象,表示触发此次的鼠标事件。该事件支持自定义冒泡设置,默认父子冒泡。常用于开发者自定义的鼠标行为逻辑处理。
开发者可以通过回调中的MouseEvent对象获取触发事件的坐标(displayX/displayY/windowX/windowY/x/y)、按键(MouseButton)、行为(MouseAction)、时间戳(timestamp)、交互组件的区域(EventTarget)、事件来源(SourceType)等。MouseEvent的回调函数stopPropagation用于设置当前事件是否阻止冒泡。
// xxx.ets @Entry @Component struct MouseExample { @State buttonText: string = ''; @State columnText: string = ''; @State hoverText: string = 'Not Hover'; @State Color: Color = Color.Gray; build() { Column() { Button(this.hoverText) .width(200) .height(100) .backgroundColor(this.Color) .onHover((isHover?: boolean) => { if (isHover) { this.hoverText = 'Hovered!'; this.Color = Color.Green; } else { this.hoverText = 'Not Hover'; this.Color = Color.Gray; } }) .onMouse((event?: MouseEvent) => { // 设置Button的onMouse回调 if (event) { this.buttonText = 'Button onMouse:\n' + '' + 'button = ' + event.button + '\n' + 'action = ' + event.action + '\n' + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + 'windowXY=(' + event.windowX + ',' + event.windowY + ')'; } }) Divider() Text(this.buttonText).fontColor(Color.Green) Divider() Text(this.columnText).fontColor(Color.Red) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .borderWidth(2) .borderColor(Color.Red) .onMouse((event?: MouseEvent) => { // Set the onMouse callback for the column. if (event) { this.columnText = 'Column onMouse:\n' + '' + 'button = ' + event.button + '\n' + 'action = ' + event.action + '\n' + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + 'windowXY=(' + event.windowX + ',' + event.windowY + ')'; } }) } }
在onHover示例的基础上,给Button绑定onMouse接口。在回调中,打印出鼠标事件的button/action等回调参数值。同时,在外层的Column容器上,也做相同的设置。整个过程可以分为以下两个动作:
移动鼠标:当鼠标从Button外部移入Button的过程中,仅触发了Column的onMouse回调;当鼠标移入到Button内部后,由于onMouse事件默认是冒泡的,所以此时会同时响应Column的onMouse回调和Button的onMouse回调。此过程中,由于鼠标仅有移动动作没有点击动作,因此打印信息中的button均为0(MouseButton.None的枚举值)、action均为3(MouseAction.Move的枚举值)。
点击鼠标:鼠标进入Button后进行了2次点击,分别是左键点击和右键点击。
左键点击时:button = 1(MouseButton.Left的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。
右键点击时:button = 2(MouseButton.Right的枚举值),按下时 action = 1(MouseAction.Press的枚举值),抬起时 action = 2(MouseAction.Release的枚举值)。
如果需要阻止鼠标事件冒泡,可以通过调用stopPropagation()方法进行设置。
class ish{ isHovered:boolean = false set(val:boolean){ this.isHovered = val; } } class butf{ buttonText:string = '' set(val:string){ this.buttonText = val } } @Entry @Component struct MouseExample { @State isHovered:ish = new ish() build(){ Column(){ Button(this.isHovered ? 'Hovered!' : 'Not Hover') .width(200) .height(100) .backgroundColor(this.isHovered ? Color.Green : Color.Gray) .onHover((isHover?: boolean) => { if(isHover) { let ishset = new ish() ishset.set(isHover) } }) .onMouse((event?: MouseEvent) => { if (event) { if (event.stopPropagation) { event.stopPropagation(); // 在Button的onMouse事件中设置阻止冒泡 } let butset = new butf() butset.set('Button onMouse:\n' + '' + 'button = ' + event.button + '\n' + 'action = ' + event.action + '\n' + 'x,y = (' + event.x + ',' + event.y + ')' + '\n' + 'windowXY=(' + event.windowX + ',' + event.windowY + ')'); } }) } } }
在子组件(Button)的onMouse中,通过回调参数event调用stopPropagation回调方法(如下)即可阻止Button子组件的鼠标事件冒泡到父组件Column上。
event.stopPropagation()
效果是:当鼠标在Button组件上操作时,仅Button的onMouse回调会响应,Column的onMouse回调不会响应
hoverEffect
hoverEffect(value: HoverEffect)
鼠标悬浮态效果设置的通用属性。参数类型为HoverEffect,HoverEffect提供的Auto、Scale、Highlight效果均为固定效果,开发者无法自定义设置效果参数。
表1 HoverEffect说明
HoverEffect枚举值 效果说明 Auto 组件默认提供的悬浮态效果,由各组件定义。 Scale 动画播放方式,鼠标悬浮时:组件大小从100%放大至105%,鼠标离开时:组件大小从105%缩小至100%。 Highlight 动画播放方式,鼠标悬浮时:组件背景色叠加一个5%透明度的白色,视觉效果是组件的原有背景色变暗,鼠标离开时:组件背景色恢复至原有样式。 None 禁用悬浮态效果。 // xxx.ets @Entry @Component struct HoverExample { build() { Column({ space: 10 }) { Button('Auto') .width(170).height(70) Button('Scale') .width(170).height(70) .hoverEffect(HoverEffect.Scale) Button('Highlight') .width(170).height(70) .hoverEffect(HoverEffect.Highlight) Button('None') .width(170).height(70) .hoverEffect(HoverEffect.None) }.width('100%').height('100%').justifyContent(FlexAlign.Center) } }
Button默认的悬浮态效果就是Highlight效果,因此Auto和Highlight的效果一样,Highlight会使背板颜色变暗,Scale会让组件缩放,None会禁用悬浮态效果。
按键事件
按键事件数据流
按键事件由外设键盘等设备触发,经驱动和多模处理转换后发送给当前获焦的窗口,窗口获取到事件后,会尝试分发三次事件。三次分发的优先顺序如下,一旦事件被消费,则跳过后续分发流程。
- 首先分发给ArkUI框架用于触发获焦组件绑定的onKeyPreIme回调和页面快捷键;
- 再向输入法分发,输入法会消费按键用作输入;
- 再次将事件发给ArkUI框架,用于响应系统默认Key事件(例如走焦),以及获焦组件绑定的onKeyEvent回调。
因此,当某输入框组件获焦,且打开了输入法,此时大部分按键事件均会被输入法消费。例如字母键会被输入法用来往输入框中输入对应字母字符、方向键会被输入法用来切换选中备选词。如果在此基础上给输入框组件绑定了快捷键,那么快捷键会优先响应事件,事件也不再会被输入法消费。
按键事件到ArkUI框架之后,会先找到完整的父子节点获焦链。从叶子节点到根节点,逐一发送按键事件。
Web组件的KeyEvent流程与上述过程有所不同。对于Web组件,不会在onKeyPreIme返回false时候,去匹配快捷。而是第三次按键派发中,Web对于未消费的KeyEvent会通过ReDispatch重新派发回ArkUI。在ReDispatch中再执行匹配快捷键等操作。
onKeyEvent & onKeyPreIme
onKeyEvent(event: (event: KeyEvent) => void): T onKeyPreIme(event: Callback<KeyEvent, boolean>): T
上述两种方法的区别仅在于触发的时机(见 按键事件数据流)。其中,onKeyPreIme的返回值决定了该按键事件后续是否会被继续分发给页面快捷键、输入法和onKeyEvent。
当绑定方法的组件处于获焦状态下,外设键盘的按键事件会触发该方法,回调参数为KeyEvent,可由该参数获得当前按键事件的按键行为(KeyType)、键码(keyCode)、按键英文名称(keyText)、事件来源设备类型(KeySource)、事件来源设备id(deviceId)、元键按压状态(metaKey)、时间戳(timestamp)、阻止冒泡设置(stopPropagation)。
// xxx.ets @Entry @Component struct KeyEventExample { @State buttonText: string = ''; @State buttonType: string = ''; @State columnText: string = ''; @State columnType: string = ''; build() { Column() { Button('onKeyEvent') .defaultFocus(true) .width(140).height(70) .onKeyEvent((event?: KeyEvent) => { // 给Button设置onKeyEvent事件 if(event){ if (event.type === KeyType.Down) { this.buttonType = 'Down'; } if (event.type === KeyType.Up) { this.buttonType = 'Up'; } this.buttonText = 'Button: \n' + 'KeyType:' + this.buttonType + '\n' + 'KeyCode:' + event.keyCode + '\n' + 'KeyText:' + event.keyText; } }) Divider() Text(this.buttonText).fontColor(Color.Green) Divider() Text(this.columnText).fontColor(Color.Red) }.width('100%').height('100%').justifyContent(FlexAlign.Center) .onKeyEvent((event?: KeyEvent) => { // 给父组件Column设置onKeyEvent事件 if(event){ if (event.type === KeyType.Down) { this.columnType = 'Down'; } if (event.type === KeyType.Up) { this.columnType = 'Up'; } this.columnText = 'Column: \n' + 'KeyType:' + this.buttonType + '\n' + 'KeyCode:' + event.keyCode + '\n' + 'KeyText:' + event.keyText; } }) } }
上述示例中给组件Button和其父容器Column绑定onKeyEvent。应用打开页面加载后,组件树上第一个可获焦的非容器组件自动获焦,设置Button为当前页面的默认焦点,由于Button是Column的子节点,Button获焦也同时意味着Column获焦。获焦机制见焦点事件。
打开应用后,依次在键盘上按这些按键:“空格、回车、左Ctrl、左Shift、字母A、字母Z”。
由于onKeyEvent事件默认是冒泡的,所以Button和Column的onKeyEvent都可以响应。
每个按键都有2次回调,分别对应KeyType.Down和KeyType.Up,表示按键被按下、然后抬起。
如果要阻止冒泡,即仅Button响应键盘事件,Column不响应,在Button的onKeyEvent回调中加入event.stopPropagation()方法即可,如下:
@Entry @Component struct KeyEventExample { @State buttonText: string = ''; @State buttonType: string = ''; @State columnText: string = ''; @State columnType: string = ''; build() { Column() { Button('onKeyEvent') .defaultFocus(true) .width(140).height(70) .onKeyEvent((event?: KeyEvent) => { // 通过stopPropagation阻止事件冒泡 if(event){ if(event.stopPropagation){ event.stopPropagation(); } if (event.type === KeyType.Down) { this.buttonType = 'Down'; } if (event.type === KeyType.Up) { this.buttonType = 'Up'; } this.buttonText = 'Button: \n' + 'KeyType:' + this.buttonType + '\n' + 'KeyCode:' + event.keyCode + '\n' + 'KeyText:' + event.keyText; } }) Divider() Text(this.buttonText).fontColor(Color.Green) Divider() Text(this.columnText).fontColor(Color.Red) }.width('100%').height('100%').justifyContent(FlexAlign.Center) .onKeyEvent((event?: KeyEvent) => { // 给父组件Column设置onKeyEvent事件 if(event){ if (event.type === KeyType.Down) { this.columnType = 'Down'; } if (event.type === KeyType.Up) { this.columnType = 'Up'; } this.columnText = 'Column: \n' + 'KeyType:' + this.buttonType + '\n' + 'KeyCode:' + event.keyCode + '\n' + 'KeyText:' + event.keyText; } }) } }
使用OnKeyPreIme屏蔽在输入框中使用方向左键。
import { KeyCode } from '@kit.InputKit'; @Entry @Component struct PreImeEventExample { @State buttonText: string = ''; @State buttonType: string = ''; @State columnText: string = ''; @State columnType: string = ''; build() { Column() { Search({ placeholder: "Search..." }) .width("80%") .height("40vp") .border({ radius:"20vp" }) .onKeyPreIme((event:KeyEvent) => { if (event.keyCode == KeyCode.KEYCODE_DPAD_LEFT) { return true; } return false; }) } } }