HarmonyOS ArkUI交互事件与手势处理全解析:从基础到高级实践
文章目录
- 一、交互事件
- 1.1 通用事件
- 1.1.1 事件分发
- 1.1.1.1 触摸测试
- 1. 触摸测试基本流程
- 2. 触摸测试控制
- 3. 自定义事件拦截
- 4. 禁用控制
- 5. 触摸热区设置
- 6. 安全组件
- 1.1.1.2 事件响应链收集
- 1.1.2 触屏事件
- 1.1.3 键鼠事件
- 1.1.3.1 鼠标事件
- 1.1.3.2 按键事件
- 1.1.4 焦点事件
- 1.1.5 拖拽事件
- 1.2 使用手势事件
- 1.2.1 绑定手势方法
- 1.2.2 单一手势
- 1.2.3 组合手势
- 1.2.4 多层手势事件
- 1.2.4.1 默认多层级手势事件
- 1.2.4 .2 自定义控制的多层级手势事件
- 1.2.5 手势拦截
- 二、手势事件冲突
- 2.1 手势响应
- 2.1.1 手势响应优先级
- 2.1.2 手势响应控制
- 2.2 总结
一、交互事件
1.1 通用事件
1.1.1 事件分发
事件分发是指ArkUI收到用户操作生成的触控事件,通过触摸测试,将触控事件分发至哥哥组件形成事件的过程。
触控事件是触摸测试的输入,根据用户操作方式的不同,可以划分为Touch类触控事件和Mouse类触控事件。
- Touch类触控事件指触摸生成的触控事件,输入源包含: finger(手指在屏幕上的滑动)、pen(手写笔在屏幕滑动)、mouse(鼠标操作)、touchpad(触控板操作),可以触发触摸事件、点击事件、拖拽事件和手势事件。
- Mouse类触控事件是指鼠标操作生成的触控事件,输入源包含:mouse(鼠标操作)、touchpad(触控板操作)、joystick(手柄操作),可以触发触摸事件、点击事件、拖拽事件、手势事件和鼠标事件。
不论是Touch类触控事件还是Mouse类触控事件,最后触发的事件均是通过触摸测试决定最终分发到的组件。触摸测试决定了ArkUI事件响应链生成、触控事件分发以及组件绑定事件的触发。
1.1.1.1 触摸测试
触摸测试是指当ArkUI收到了Touch类触控事件或者Mouse类触控事件的起始事件,基于所收到的事件的坐标,进行组件响应区域的测试判定并收集事件响应链的过程。
设置一下属性影响触摸测试流程:
- hitTestBehavior:触摸测试控制
- interceptTouch:事件自定义拦截
- responseRegion:触摸热区设置
- enabled:禁用控制
- 安全控件:HarmonyOS ArkUI安全控件开发指南:粘贴、保存与位置控件的实现与隐私保护实践
- 其他属性设置:透明度/组件下线
1. 触摸测试基本流程
接收到起始事件后,系统自上而下、自右向左的遍历组件树,收集每个组件上绑定的手势和事件,然后将这些信息逐级向上冒泡至父组件进行整合,最终构建完整的事件响应链。
2. 触摸测试控制
在组件上绑定触摸测试控制时,可能会影响到兄弟节点以及父子节点的触摸测试。子组件对父组件的触摸测试影响程度,取决于最后一个未被阻塞触摸测试的子组件状态。
可以通过配置触摸测试控制,来实现阻塞组件自身或其他组件的触摸测试。
-
HitTestMode.Default:默认不配hitTestBehavior属性的效果,自身如果命中会阻塞兄弟组件,但是不阻塞子组件。
-
HitTestMode.None:自身不接受事件,但不会阻塞兄弟组件/子组件继续做触摸测试
-
HitTestMode.Block:阻塞子组件的触摸测试,如果自身触摸测试命中,会阻塞兄弟组件及父组件的触摸测试。
-
HitTestMode.Transparent:自身进行触摸测试,同时不阻塞兄弟组件及父组件。
3. 自定义事件拦截
onTouchIntercept(callback: Callback<TouchEvent, HitTestMode>);
当用户执行按下操作时,将触发组件上绑定的自定义事件拦截的回调。开发者可根据应用状态,动态调整组件的hitTestBehavior属性,进而影响触控测试的流程。
参数名 | 参数类型 | 必填 | 参数描述 |
---|---|---|---|
callback | Callback<TouchEvent,HitTestMode> | 是 | 给组件绑定自定义事件拦截回调,并使能在做触摸测试时回调此函数。 |
示例
@Entry
@Component
struct Index {isPolygon(event: TouchEvent) {return true;}build(){Row(){Column(){Text("hello world").backgroundColor(Color.Blue).fontSize(50).fontWeight(FontWeight.Bold).onClick(()=>{console.log("Text click");})}.width(400).height(300).backgroundColor(Color.Pink).onClick(()=>{console.log("Column click");})// 调用onTouchIntercept修改该组件的HitTestMode属性.onTouchIntercept((event : TouchEvent) => {console.log("OnTouchIntercept + " + JSON.stringify(event));if (this.isPolygon(event)) {return HitTestMode.None}return HitTestMode.Default})}.width('100%')}
}
4. 禁用控制
enabled(value: boolean);
设置了禁用控制的组件,组件自身和其子组件不会发起触摸测试过程,会直接返回组件的父组件继续触摸测试
参数名 | 类型 | 必填 | 说明 |
---|---|---|---|
value | boolean | 是 | 值为true表示组件可交互,响应点击等操作。值为false表示组件不可交互,不响应点击等操作。默认值:true |
示例
@Entry
@Component
struct EnabledExample {build() {Flex({ justifyContent: FlexAlign.SpaceAround }) {// 点击没有反应Button('disable').enabled(false).backgroundColor(0x317aff).opacity(0.4)Button('enable').backgroundColor(0x317aff)}.width('100%').padding({ top: 5 })}
}
5. 触摸热区设置
responseRegion(value: Array<Rectangle> | Rectangle);
设置一个或多个触摸热区。
触摸热区设置会影响触屏/鼠标类的触摸测试。根据触摸测试的基本流程,仅当事件的坐标命中组件的触摸热区时,该组件绑定的手势和事件才会被收集并进入事件响应链。开发者可以通过调整组建的触摸热区来控制触摸测试流程。若触摸热区被设置为0,或定义为不可触控区域,事件将直接回传给父节点,以进行后续的触摸测试。
参数名 | 类型 | 必填 | 说明 |
---|---|---|---|
value | Array<Rectangle]> | Rectangle | 是 | 设置一个或多个触摸热区,包括位置和大小。默认触摸热区为整个组件,默认值:{x:0,y:0,width:‘100%’,height:‘100%’} |
@Entry
@Component
struct TouchTargetExample {@State text: string = ""build() {Column({ space: 20 }) {Text("{x:0,y:0,width:'50%',height:'100%'}")// 热区宽度为按钮的一半,点击右侧无响应Button("button1").responseRegion({ x: 0, y: 0, width: '50%', height: '100%' }).onClick(() => {this.text = 'button1 clicked'})// 为一个组件添加多个热区Text("[{x:'100%',y:0,width:'50%',height:'100%'}," +"\n{ x: 0, y: 0, width: '50%', height: '100%' }]")Button("button2").responseRegion([{ x: '100%', y: 0, width: '50%', height: '100%' }, // 第一个热区宽度为按钮的一半,点击按钮右侧宽度一半区域,点击事件生效{ x: 0, y: 0, width: '50%', height: '100%' } // 第二个热区宽度为按钮的一半,点击button2左半边,点击事件生效]).onClick(() => {this.text = 'button2 clicked'})// 热区大小为整个按钮,且下移一个按钮高度,点击button3下方按钮大小区域,点击事件生效Text("{x:0,y:'100%',width:'100%',height:'100%'}")Button("button3").responseRegion({ x: 0, y: '100%', width: '100%', height: '100%' }).onClick(() => {this.text = 'button3 clicked'})Text(this.text).margin({ top: 50 })}.width('100%').margin({ top: 10 })}
}
6. 安全组件
安全组件当前对触摸测试影响:如果有组件的z序比安全组件的z序靠前,且遮盖安全组件,则安全组件事件直接返回到父节点继续触摸测试。
1.1.1.2 事件响应链收集
在HarmonyOS开发中,触摸事件时用户与设备交互的基础,时所有手势事件组成的基础,触摸事件的分发由触摸测试结果决定,其结果会直接决定哪些空间的事件加入事件响应链,并在最终按照响应链顺序判定是否消费。
AekUI事件响应链收集,根据右子树优先的后续遍历流程
build() {StackA() {ComponentB() {ComponentC()}ComponentD() {ComponentE()}}
}
其中A是最外层组件,B和D是A的子组件,C是B的子组件,E是D的子组件。
用户触摸的动作发生在组件C上,事件响应链的收集流程如下,根据右子树优先的后续遍历流程,因为触摸点不在右边的树上,所以事件会从左边树的C节点开始往上传,触摸事件是冒泡事件默认会向上一直传递下去,知道被消费或者丢弃,允许多个组件同时触发。
用户触摸的动作发生在组件E上,事件响应链收集的流程如下,根据右子树的优先的后续遍历流程,所以时间会从右边树的D节点开始往上传。虽然触摸点在组件D和B的交集上,但组件D的hitTestBehavior属性默认为HitTestMode.Default,D组件收集到事件后会阻塞兄弟节点,所以没有手机组件A的左子树。
上面介绍的事件响应链是系统默认的行为,如果需要改变相应的成员,比如触摸组件E的时候,希望把事件传递给B,可以通过设置D组件的hitTestMode属性为HitTestMode.None或者HitTestMode.Transparent来实现,比如设置为HitTestMode.Transparent,那么组件D自身进行触摸测试,同时不阻塞兄弟及父组件。最终收集到的响应链是E->D->B->A。
又例如触摸E组件的时候,只希望E相应触摸事件,不让其他组件响应触摸事件。可以通过stopPropagation来阻止事件冒泡,阻止触摸事件往上传递;也可以通过设置E组件的hitTestMode属性为HitTestMode.Block来实现,那么最终收集到的响应链成员只有组件E
1.1.2 触屏事件
触屏事件指当手指/手写笔在组件上按下、滑动、抬起触发的回调事件。
‘
点击事件
点击事件时通过手指或手写笔做出一次完整的按下和抬起动作。
onClick(event: (event?: ClickEvent) => void)
触摸事件
当手指或手写笔在组件上触碰时,会触发不同动作所对应的事件响应,包括按下(Down)、滑动(Move)、抬起(Up)事件:
onTouch(event: (event?: TouchEvent) => void)
- event.type为TouchType.Down:表示手指按下。
- event.type为TouchType.Up:表示手指抬起。
- event.type为TouchType.Move:表示手指按住移动。
- event.type为TouchType.Cancel:表示打断取消当前手指操作。
1.1.3 键鼠事件
键鼠事件指键盘,鼠标外接设备的输入事件。
1.1.3.1 鼠标事件
鼠标事件传递到ArkUI之后,会先判断鼠标事件是否是左键的按下/抬起/移动,然后做出不同响应:
- 是:鼠标事件先转换成相同位置的触摸事件,执行触摸事件的碰撞测试、手势判断和回调响应。接着去执行鼠标事件的碰撞测试和回调响应。
- 否:事件仅用于执行鼠标事件的碰撞测试和回调响应。
说明
所有单指可响应的触摸事件/手势事件,均可通过鼠标左键来操作和响应。例如当我们需要开发单击Button跳转页面的功能、且需要支持手指点击和鼠标左键点击,那么只绑定一个点击事件(onClick)就可以实现该效果。若需要针对手指和鼠标左键的点击实现不一样的效果,可以在onClick回调中,使用回调参数中的source字段即可判断出当前触发事件的来源是手指还是鼠标。
onHover
onHover(event: (isHover: boolean) => void)
鼠标悬浮事件回调。参数isHover类型为boolean,表示鼠标进入组件或离开组件。该事件不支持自定义冒泡设置,默认父子冒泡。
若组件绑定了该接口,当鼠标指针从组件外部进入到该组件的瞬间会触发事件回调,参数isHover等于true;鼠标指针离开组件的瞬间也会触发该事件回调,参数isHover等于false。
说明
事件冒泡:在一个树形结构中,当子节点处理完一个事件后,再将该事件交给它的父节点处理。
onMouse
onMouse(event: (event?: MouseEvent) => void)
鼠标事件回调。绑定该API的组件每当鼠标指针在该组件内产生行为(MouseAction)时,触发事件回调,参数为MouseEvent对象,表示触发此次的鼠标事件。该事件支持自定义冒泡设置,默认父子冒泡。常用于开发者自定义的鼠标行为逻辑处理。
说明
按键(MouseButton)的值:Left/Right/Middle/Back/Forward 均对应鼠标上的实体按键,当这些按键被按下或松开时触发这些按键的事件。None表示无按键,会出现在鼠标没有按键按下或松开的状态下,移动鼠标所触发的事件中。
hoverEffect
hoverEffect(value: HoverEffect)
鼠标悬浮态效果设置的通用属性。参数类型为HoverEffect,HoverEffect提供的Auto、Scale、Highlight效果均为固定效果
1.1.3.2 按键事件
按键事件数据流
按键事件由外设键盘等设备触发,经驱动和多模处理转换后发送给当前获焦的窗口,窗口获取到事件后,会尝试分发三次事件。三次分发的优先顺序如下,一旦事件被消费,则跳过后续分发流程。
- 首先分发给ArkUI框架用于触发获焦组件绑定的onKeyPreIme回调和页面快捷键。
- 再向输入法分发,输入法会消费按键用作输入。
- 再次将事件发给ArkUI框架,用于响应系统默认Key事件(例如走焦),以及获焦组件绑定的onKeyEvent回调。
因此,当某输入框组件获焦,且打开了输入法,此时大部分按键事件均会被输入法消费。例如字母键会被输入法用来往输入框中输入对应字母字符、方向键会被输入法用来切换选中备选词。如果在此基础上给输入框组件绑定了快捷键,那么快捷键会优先响应事件,事件也不再会被输入法消费。
按键事件到ArkUI框架之后,会先找到完整的父子节点获焦链。从叶子节点到根节点,逐一发送按键事件。
Web组件的KeyEvent流程与上述过程有所不同。对于Web组件,不会在onKeyPreIme返回false时候,去匹配快捷。而是第三次按键派发中,Web对于未消费的KeyEvent会通过ReDispatch重新派发回ArkUI。在ReDispatch中再执行匹配快捷键等操作。
onKeyEvent & onKeyPreIme
onKeyEvent(event: (event: KeyEvent) => void): TonKeyPreIme(event: Callback<KeyEvent, boolean>): T
1.1.4 焦点事件
焦点、焦点链和走焦
- 焦点:指向当前应用界面上唯一的一个可交互元素,当用户使用键盘、电视遥控器、车机摇杆/旋钮等非指向性输入设备与应用程序进行间接交互时,基于焦点的导航和交互式重要的输入手段。
- 焦点链:在应用的组件树形结构种,当一个组件获得焦点时,从根节点到该组件节点的整条路径上的所有节点都会被视为处于焦点状态,形成一条连续的焦点链。
- 走焦:指焦点在应用内的组件之间转移的行为。这一过程对用户是透明的,单可以通过onFocus和onBlur事件来捕捉这些变化。
焦点态
用来指向当前获焦组件的样式。
- 显示规则:默认情况下焦点态不会显示,只有当应用进入激活状态后,焦点态才会显示。因此,虽然获得焦点的组件不一定显示焦点态,但焦点态的组件必然是获得焦点的。大部分组件内置了焦点态样式,同样可以使用样式接口进行自定义,一旦自定义,组件将不再显示内置的焦点态样式。在焦点链之中,若多个组件同时拥有焦点态,系统将采用子组件优先的策略,优先显示子组件的焦点态,并且仅显示一个焦点态。
- 进入激活态:使用外接键盘按下TAB键/使用FocusController的activate方法才进入焦点的激活态,进入激活态之后,才可以使用TAB键/方向键进行走焦。首次用来激活焦点态的TAB键不会触发走焦。
- 退出激活态:当应用收到FocusController的active方法/点击事件,焦点的激活态会退出。
层级页面
层级页面是焦点框架中特定容器组件的同城,涵盖Page、Dialog、SheetPage、ModalPage、Menu、Popup、NavBar、NavDestination等。这些组件通常具有以下关键特性。
- 视觉层级独立性:从视觉呈现上看,这些组件独立于其他页面内容,并通常位于其上方,形成视觉上的层级差异。
- 焦点跟随:此类组件在首次创建并展示之后,会立即将应用内焦点抢占。
- 走焦范围限制:当焦点位于这些组件内部时,用户无法通过键盘按键将焦点转移到组件外部的其他元素上,焦点移动仅限于组件内部。
在一个应用程序中,任何时候都至少存在一个层级页面组件,并且该组件会持有当前焦点。当该层级页面关闭或不在可见时,焦点会自动转移到下一个可用的层级页面组件上,确保用户交互的连贯性和一致性。
说明
Popup组件在focusable属性(组件属性,非通用属性)为false的时候,不会有第2条特性。
NavBar、NavDestination没有第3条特性,对于它们的走焦范围,是与它们的首个父层级页面相同的。
根容器
根容器时层级页面内的概念,当某个层级页面首次创建并展示时,根据层级页面的特性,焦点会立即被该页面抢占。此时,该层级页面所在焦点链的末端节点将成为默认焦点,而这个默认焦点通常位于该层级页面的根容器上。
在缺省状态下,层级页面的默认焦点位于其根容器上,但可以通过defaultFocus属性来自定义这一行为。
当焦点位于根容器时,首次按下TAB键不仅会使焦点进入激活状态,还会触发焦点向子组件的传递。如果子组件本身也是一个容器,则焦点会继续向下传递,直至到达叶子节点。传递规则是:优先传递给上一次获得焦点的子节点,如果不存在这样的节点,则默认传递给第一个子节点。
1.1.5 拖拽事件
拖拽事件提供了一种通过鼠标或手势触屏传递数据的机制,即从一个组件位置拖出(drag)数据并将其拖入(drop)到另一个组件位置,以触发响应。在这一过程中,拖出方提供数据,而拖入方负责接收和处理数据。这一操作使用户能够便捷地移动、复制或删除指定内容。
基本概念
- 拖拽操作:在可响应拖出的组件上长按并滑动以触发拖拽行为,当用户释放手指或鼠标时,拖拽操作即告结束。
- 拖拽背景(背板):用户拖动数据时的形象化表示。开发者可以通过onDragStart的CustomerBuilder]或DragItemInfo进行设置,也可以通过dragPreview通用属性进行自定义。
- 拖拽内容:被拖动的数据,使用UDMF统一API UnifiedData 进行封装,确保数据的一致性和安全性。
- 拖出对象:触发拖拽操作并提供数据的组件,通常具有响应拖拽的特性。
- 拖入目标:可接收并处理拖动数据的组件,能够根据拖入的数据执行相应的操作。
- 拖拽点:鼠标或手指与屏幕的接触位置,用于判断是否进入组件范围。判定依据是接触点是否位于组件的范围内。
1.2 使用手势事件
1.2.1 绑定手势方法
-
gesture(常规手势绑定方法)
gesture为通用的一种手势绑定方法,可以将手势绑定到对应的组件上。
.gesture(gesture: GestureType, mask?: GestureMask)
例如,可以将点击手势TapGesture通过gesture手势将方法绑定到Text组件上。
@Entry @Component struct Index {build() {Column() {Text('Gesture').fontSize(28)// 采用gesture手势绑定方法绑定TapGesture.gesture(TapGesture().onAction(() => {console.info('TapGesture is onAction');}))}.height(200).width(250)} }
prioityGesture(带优先级的手势绑定方法)
.priorityGesture(gesture: GestureType, mask?: GestureMask)
priorityGesture是带优先级的手势绑定方法,可以在组件上绑定优先识别的手势。当父组件使用priorityGesture绑定与父子组件同类型的手势时,父组件会优先识别通过priorityGesture绑定的手势。
长按手势时,设置触发长按的最短时间小的组件会优先响应,会忽略priorityGesture设置。
例如,当父组件Column和子组件Text同时绑定TapGesture手势时,父组件以带优先级手势priorityGesture的形式进行绑定时,优先相应父组件绑定的TapGesture。
@Entry
@Component
struct Index {build() {Column() {Text('Gesture').fontSize(28).gesture(TapGesture().onAction(() => {console.info('Text TapGesture is onAction');}))}.height(200).width(250)// 设置为priorityGesture时,点击文本区域会忽略Text组件的TapGesture手势事件,优先响应父组件Column的TapGesture手势事件.priorityGesture(TapGesture().onAction(() => {console.info('Column TapGesture is onAction');}), GestureMask.IgnoreInternal)}
}
parallelGesture(并行手势绑定方法)
.parallelGesture(gesture: GestureType, mask?: GestureMask)
parallelGesture是并行手势绑定方法,可以在父子组件上绑定可以同时响应的相同手势。
在默认情况下,手势事件为非冒泡事件,当父子组件上绑定相同的手势时,父子组件绑定的手势事件会发生竞争,最多只有一个组件的手势能够获得响应。而当父组件绑定了并行手势parallelGesture时,父子组件相同的手势事件都可以触发,实现类似冒泡效果。
@Entry
@Component
struct Index {build() {Column() {Text('Gesture').fontSize(28).gesture(TapGesture().onAction(() => {console.info('Text TapGesture is onAction');}))}.height(200).width(250)// 设置为parallelGesture时,点击文本区域会同时响应父组件Column和子组件Text的TapGesture手势事件.parallelGesture(TapGesture().onAction(() => {console.info('Column TapGesture is onAction');}), GestureMask.Normal)}
}
1.2.2 单一手势
点击手势(TapGesture)
TapGesture(value?:{count?:number, fingers?:number})
点击手势支持单次点击和多次点击,拥有两个可选参数:
- count:声明该点击手势识别的连续点击次数。默认值为1,若设置小于1的非法值会被转化为默认值。如果配置多次点击,上一次抬起和下一次按下的超时时间为300毫秒。
- fingers:用于声明触发点击的手指数量,最小值为1,最大值为10,默认值为1。当配置多指时,若第一根手指按下300毫秒内未有足够的手指数按下则手势识别失败。
@Entry
@Component
struct Index {@State value: string = "";build() {Column() {Text('Click twice').fontSize(28).gesture(// 绑定count为2的TapGestureTapGesture({ count: 2 }).onAction((event: GestureEvent|undefined) => {if(event){this.value = JSON.stringify(event.fingerList[0]);}}))Text(this.value)}.height(200).width(250).padding(20).border({ width: 3 }).margin(30)}
}
长按手势(LongPressGesture)
LongPressGesture(value?:{fingers?:number, repeat?:boolean, duration?:number})
长按手势用于触发长按手势事件,拥有三个可选参数:
- fingers:用于声明触发长按手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- repeat:用于声明是否连续触发事件回调,默认值为false。
- duration:用于声明触发长按所需的最短时间,单位为毫秒,默认值为500。
@Entry
@Component
struct Index {@State count: number = 0;build() {Column() {Text('LongPress OnAction:' + this.count).fontSize(28).gesture(// 绑定可以重复触发的LongPressGestureLongPressGesture({ repeat: true }).onAction((event: GestureEvent|undefined) => {if(event){if (event.repeat) {this.count++;}}}).onActionEnd(() => {this.count = 0;}))}.height(200).width(250).padding(20).border({ width: 3 }).margin(30)}
}
拖动手势(PanGesture)
PanGesture(value?:{ fingers?:number, direction?:PanDirection, distance?:number})
拖动手势用于触发拖动手势事件,滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功,拥有三个可选参数:
- fingers:用于声明触发拖动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- direction:用于声明触发拖动的手势方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为Pandirection.All。
- distance:用于声明触发拖动的最小拖动识别距离,单位为vp,默认值为5。
@Entry
@Component
struct Index {@State offsetX: number = 0;@State offsetY: number = 0;@State positionX: number = 0;@State positionY: number = 0;build() {Column() {Text('PanGesture Offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY).fontSize(28).height(200).width(300).padding(20).border({ width: 3 })// 在组件上绑定布局位置信息.translate({ x: this.offsetX, y: this.offsetY, z: 0 }).gesture(// 绑定拖动手势PanGesture().onActionStart((event: GestureEvent|undefined) => {console.info('Pan start');})// 当触发拖动手势时,根据回调函数修改组件的布局位置信息.onActionUpdate((event: GestureEvent|undefined) => {if(event){this.offsetX = this.positionX + event.offsetX;this.offsetY = this.positionY + event.offsetY;}}).onActionEnd(() => {this.positionX = this.offsetX;this.positionY = this.offsetY;}))}.height(200).width(250)}
}
捏合手势(PinchGesture)
PinchGesture(value?:{fingers?:number, distance?:number})
捏合手势用于触发捏合手势事件,拥有两个可选参数:
- fingers:用于声明触发捏合手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。
- distance:用于声明触发捏合手势的最小距离,单位为vp,默认值为5。
@Entry
@Component
struct Index {@State scaleValue: number = 1;@State pinchValue: number = 1;@State pinchX: number = 0;@State pinchY: number = 0;build() {Column() {Column() {Text('PinchGesture scale:\n' + this.scaleValue)Text('PinchGesture center:\n(' + this.pinchX + ',' + this.pinchY + ')')}.height(200).width(300).border({ width: 3 }).margin({ top: 100 })// 在组件上绑定缩放比例,可以通过修改缩放比例来实现组件的缩小或者放大.scale({ x: this.scaleValue, y: this.scaleValue, z: 1 }).gesture(// 在组件上绑定三指触发的捏合手势PinchGesture({ fingers: 3 }).onActionStart((event: GestureEvent|undefined) => {console.info('Pinch start');})// 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例.onActionUpdate((event: GestureEvent|undefined) => {if(event){this.scaleValue = this.pinchValue * event.scale;this.pinchX = event.pinchCenterX;this.pinchY = event.pinchCenterY;}}).onActionEnd(() => {this.pinchValue = this.scaleValue;console.info('Pinch end');}))}}
}
旋转手势(RotationGesture)
RotationGesture(value?:{fingers?:number, angle?:number})
旋转手势用于触发旋转手势事件,拥有两个可选参数:
- fingers:用于声明触发旋转手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。
- angle:用于声明触发旋转手势的最小改变度数,单位为deg,默认值为1。
@Entry
@Component
struct Index {@State angle: number = 0;@State rotateValue: number = 0;build() {Column() {Text('RotationGesture angle:' + this.angle).fontSize(28)// 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转.rotate({ angle: this.angle }).gesture(RotationGesture().onActionStart((event: GestureEvent|undefined) => {console.info('RotationGesture is onActionStart');})// 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度.onActionUpdate((event: GestureEvent|undefined) => {if(event){this.angle = this.rotateValue + event.angle;}console.info('RotationGesture is onActionEnd');})// 当旋转结束抬手时,固定组件在旋转结束时的角度.onActionEnd(() => {this.rotateValue = this.angle;console.info('RotationGesture is onActionEnd');}).onActionCancel(() => {console.info('RotationGesture is onActionCancel');})).height(200).width(300).padding(20).border({ width: 3 }).margin(100)}}
}
滑动手势(SwipeGesture)
SwipeGesture(value?:{fingers?:number, direction?:SwipeDirection, speed?:number})
滑动手势用于触发滑动事件,当滑动速度大于100vp/s时可以识别成功,拥有三个可选参数:
- fingers:用于声明触发滑动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
- direction:用于声明触发滑动手势的方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为SwipeDirection.All。
- speed:用于声明触发滑动的最小滑动识别速度,单位为vp/s,默认值为100。
@Entry
@Component
struct Index {@State rotateAngle: number = 0;@State speed: number = 1;build() {Column() {Column() {Text("SwipeGesture speed\n" + this.speed)Text("SwipeGesture angle\n" + this.rotateAngle)}.border({ width: 3 }).width(300).height(200).margin(100)// 在Column组件上绑定旋转,通过滑动手势的滑动速度和角度修改旋转的角度.rotate({ angle: this.rotateAngle }).gesture(// 绑定滑动手势且限制仅在竖直方向滑动时触发SwipeGesture({ direction: SwipeDirection.Vertical })// 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改.onAction((event: GestureEvent|undefined) => {if(event){this.speed = event.speed;this.rotateAngle = event.angle;}}))}}
}
1.2.3 组合手势
组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型,支持顺序识别、并行识别、互斥识别三种类型。
GestureGroup(mode:GestureMode, gesture:GestureType[])
- mode:为GestureMode枚举类。用于声明该组合手势的类型。
- gesture:由多个手势组合而成的数组。用于声明组合成该组合手势的各个手势。
顺序识别
顺序识别组合手势对应的GestureMode为Sequence。顺序识别组合手势将手势的注册顺序识别手势,知道手势识别成功。当顺序识别组合手势中有一个手势识别失败时,后续手势识别均失败。顺序识别手势组仅有最后一个手势可以响应onActionEnd。
以一个由长按手势和拖动手势组合而成的连续手势为例:
在一个Column组件上绑定了translate属性,通过修改该属性可以设置组件的位置移动。然后在该组件上绑定LongPressGesture和PanGesture组合而成的Sequence组合手势。当触发LongPressGesture时,更新显示的数字。当长按后进行拖动时,根据拖动手势的回调函数,实现组件的拖动。
@Entry
@Component
struct Index {@State offsetX: number = 0;@State offsetY: number = 0;@State count: number = 0;@State positionX: number = 0;@State positionY: number = 0;@State borderStyles: BorderStyle = BorderStyle.Solidbuild() {Column() {Text('sequence gesture\n' + 'LongPress onAction:' + this.count + '\nPanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY).fontSize(28)}.margin(10).borderWidth(1)// 绑定translate属性可以实现组件的位置移动.translate({ x: this.offsetX, y: this.offsetY, z: 0 }).height(250).width(300)//以下组合手势为顺序识别,当长按手势事件未正常触发时不会触发拖动手势事件.gesture(// 声明该组合手势的类型为Sequence类型GestureGroup(GestureMode.Sequence,// 该组合手势第一个触发的手势为长按手势,且长按手势可多次响应LongPressGesture({ repeat: true })// 当长按手势识别成功,增加Text组件上显示的count次数.onAction((event: GestureEvent|undefined) => {if(event){if (event.repeat) {this.count++;}}console.info('LongPress onAction');}).onActionEnd(() => {console.info('LongPress end');}),// 当长按之后进行拖动,PanGesture手势被触发PanGesture().onActionStart(() => {this.borderStyles = BorderStyle.Dashed;console.info('pan start');})// 当该手势被触发时,根据回调获得拖动的距离,修改该组件的位移距离从而实现组件的移动.onActionUpdate((event: GestureEvent|undefined) => {if(event){this.offsetX = (this.positionX + event.offsetX);this.offsetY = this.positionY + event.offsetY;}console.info('pan update');}).onActionEnd(() => {this.positionX = this.offsetX;this.positionY = this.offsetY;this.borderStyles = BorderStyle.Solid;})).onCancel(() => {console.log("sequence gesture canceled")}))}
}
说明
拖拽事件是一种典型的顺序识别组合手势事件,由长按手势事件和滑动手势事件组合而成。只有先长按达到长按手势事件预设置的时间后进行滑动才会触发拖拽事件。如果长按事件未达到或者长按后未进行滑动,拖拽事件均识别失败。
并行识别
并行识别组合手势对应的GestureMode为Parallel/并行识别组合手势中注册的手势同时进行识别,知道所有手势识别结束。并行识别手势组合中的手势进行识别时互不影响。
以在一个Column组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。
@Entry
@Component
struct Index {@State count1: number = 0;@State count2: number = 0;build() {Column() {Text('Parallel gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n').fontSize(28)}.height(200).width('100%')// 以下组合手势为并行并别,单击手势识别成功后,若在规定时间内再次点击,双击手势也会识别成功.gesture(GestureGroup(GestureMode.Parallel,TapGesture({ count: 1 }).onAction(() => {this.count1++;}),TapGesture({ count: 2 }).onAction(() => {this.count2++;})))}
}
说明
当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
当只有单次点击时,单击手势识别成功,双击手势识别失败。
当有两次点击时,若两次点击相距时间在规定时间内(默认规定时间为300毫秒),触发两次单击事件和一次双击事件。
当有两次点击时,若两次点击相距时间超出规定时间,触发两次单击事件不触发双击事件。
互斥识别
互斥识别组合手势对应的GestureMode为Exclusive。互斥识别组合手势中注册的手势将同时进行识别,若有一个手势识别成功,则结束手势识别,其他所有手势识别失败。
以在一个Column组件上绑定单击手势和双击手势组合而成的互斥识别组合手势为例。若先绑定单击手势后绑定双击手势,由于单击手势只需要一次点击即可触发而双击手势需要两次,每次的点击事件均被单击手势消费而不能积累成双击手势,所以双击手势无法触发。若先绑定双击手势后绑定单击手势,则触发双击手势不触发单击手势。
@Entry
@Component
struct Index {@State count1: number = 0;@State count2: number = 0;build() {Column() {Text('Exclusive gesture\n' + 'tapGesture count is 1:' + this.count1 + '\ntapGesture count is 2:' + this.count2 + '\n').fontSize(28)}.height(200).width('100%')//以下组合手势为互斥并别,单击手势识别成功后,双击手势会识别失败.gesture(GestureGroup(GestureMode.Exclusive,TapGesture({ count: 1 }).onAction(() => {this.count1++;}),TapGesture({ count: 2 }).onAction(() => {this.count2++;})))}
}
说明
当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
当只有单次点击时,单击手势识别成功,双击手势识别失败。
当有两次点击时,手势响应取决于绑定手势的顺序。若先绑定单击手势后绑定双击手势,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。若先绑定双击手势后绑定单击手势,则会响应双击手势不响应单击手势。
1.2.4 多层手势事件
多层级手势事件指父子组件套件时,父子组件均绑定了手势或事件。在该场景下,手势或者事件的响应受到多个因素的影响,相互之间发生传递和竞争,容易出现预期外的响应。
1.2.4.1 默认多层级手势事件
触摸事件
触摸事件时所有手势组成的基础,由Down,Move,Up,Cancel四种。手势均由触摸事件组成,例如,点击成为Down+Up,华东为Down+一系列Move+Up。触摸事件具有特殊性:
- 监听了obTouch事件的组件。若在手指落下时被触摸则均会收到onTouch事件的回调,被触摸受到触摸热区和触摸控制影响。
- onTouch事件的回调是闭环的。若一个组件收到了手指id为0的Down事件,后续也会收到手指id为0的Move事件和Up事件。
- onTouch事件回调是一致的。若一个组件收到了手指id为0的Down事件为收到手指id为1的Down事件,则后续只会收到手指id为0的touch事件,不会收到手指id为1的后续touch事件。
对于一般的容器组件(例如:Column),父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件根据布局进行触发。
ComponentA() {ComponentB().onTouch(() => {})ComponentC().onTouch(() => {})
}.onTouch(() => {})
组件B和组件C作为组件A的子组件,当触摸到组件B或者组件C时,组件A也会被触摸到。onTouch事件允许多个组件同时触发,因此,当触摸组件B时,会触发组件A和组件B的onTouch回调,不会触发组件C的onTouch回调。当触摸组件C时,会触发组件A和组件C的onTouch回调,不触发组件B的回调。特殊的容器组件,如Stack等组件,由于子组件之间存在着堆叠关系,子组件的布局也互相存在遮盖关系。所以,父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件会存在遮盖关系。
Stack A() {ComponentB().onTouch(() => {})ComponentC().onTouch(() => {})
}.onTouch(() => {})
组件B和组件C作为Stack A的子组件,组件C覆盖在组件B上。当触摸到组件B或者组件C时,Stack A也会被触摸到。onTouch事件允许多个组件同时触发,因此,当触摸组件B和组件C的重叠区域时,会触发Stack A和组件C的onTouch回调,不会触发组件B的onTouch回调(组件B被组件C遮盖)。
手势与事件
除了触摸事件(onTouch事件)外的所有手势与事件,均是通过基础手势或者组合手势实现的。例如,拖拽事件是由长按手势和滑动手势组成的一个顺序手势。
在未显式声明的情况下,同一时间,一根手指对应的手势组中只会有一个手势获得成功从而触发所设置的回调。
因此,除非显式声明允许多个手势同时成功,同一时间只会有一个手势响应。
响应优先级遵循以下条件:
1.当父子组件均绑定同一类手势时,子组件优先于父组件触发。
2.当一个组件绑定多个手势时,先达到手势触发条件的手势优先触发。
ComponentA() {ComponentB().gesture(TapGesture({count: 1}))
}.gesture(TapGesture({count: 1}))
当父组件和子组件均绑定点击手势时,子组件的优先级高于父组件。
因此,当在B组件上进行点击时,组件B所绑定的TapGesture的回调会被触发,而组件A所绑定的TapGesture的回调不会被触发。
ComponentA()
.gesture(GestureGroup(GestureMode.Exclusive,TapGesture({count: 1}),PanGesture({distance: 5}))
)
当组件A上绑定了由点击和滑动手势组成的互斥手势组时,先达到手势触发条件的手势触发对应的回调。
若使用者做了一次点击操作,则响应点击对应的回调。若使用者进行了一次滑动操作并且滑动距离达到了阈值,则响应滑动对应的回调。
1.2.4 .2 自定义控制的多层级手势事件
可以通过设置属性,控制默认的多层级手势事件竞争流程,更好的实现手势事件。
目前,responseRegion属性和hitTestBehavior属性可以控制Touch事件的分发,从而可以影响到onTouch事件和手势响应。而绑定手势方法属性可以控制手势的竞争从而影响手势的响应,但不能影响到onTouch事件。
responseRegion对手势和事件的控制
responseRegion属性可以实现组件的响应区域范围的变化。响应区域范围可以超出或者小于组件的布局范围。
ComponentA() {ComponentB().onTouch(() => {}).gesture(TapGesture({count: 1})).responseRegion({Rect1, Rect2, Rect3})
}
.onTouch(() => {})
.gesture(TapGesture({count: 1}))
.responseRegion({Rect4})
当组件A绑定了.responseRegion({Rect4})的属性后,所有落在Rect4区域范围的触摸事件和手势可被组件A对应的回调响应。
当组件B绑定了.responseRegion({Rect1, Rect2, Rect3})的属性后,所有落在Rect1,Rect2和Rect3区域范围的触摸事件和手势可被组件B对应的回调响应。
当绑定了responseRegion后,手势与事件的响应区域范围将以所绑定的区域范围为准,而不是以布局区域为准,可能出现布局相关区域不响应手势与事件的情况。
此外,responseRegion属性支持由多个Rect组成的数组作为入参,以支持更多开发需求。
hitTestBehavior对手势和事件的控制
hitTestBehavior属性可以实现在复杂的多层级场景下,一些组件能够响应手势和事件,而一些组件不能响应手势和事件。
ComponentA() {ComponentB().onTouch(() => {}).gesture(TapGesture({count: 1}))ComponentC() {ComponentD().onTouch(() => {}).gesture(TapGesture({count: 1}))}.onTouch(() => {}).gesture(TapGesture({count: 1})).hitTestBehavior(HitTestMode.Block)
}
.onTouch(() => {})
.gesture(TapGesture({count: 1}))
HitTestMode.Block自身会响应触摸测试,阻塞子节点和兄弟节点的触摸测试,从而导致子节点和兄弟节点的onTouch事件和手势均无法触发。
当组件C未设置hitTestBehavior时,点击组件D区域,组件A、组件C和组件D的onTouch事件会触发,组件D的点击手势会触发。
当组件C设置了hitTestBehavior为HitTestMode.Block时,点击组件D区域,组件A和组件C的onTouch事件会触发,组件D的onTouch事件未触发。同时,由于组件D的点击手势因为被阻塞而无法触发,组件C的点击手势会触发。
Stack A() {ComponentB().onTouch(() => {}).gesture(TapGesture({count: 1}))ComponentC().onTouch(() => {}).gesture(TapGesture({count: 1})).hitTestBehavior(HitTestMode.Transparent)
}
.onTouch(() => {})
.gesture(TapGesture({count: 1}))
HitTestMode.Transparent自身响应触摸测试,不会阻塞兄弟节点的触摸测试。
当组件C未设置hitTestBehavior时,点击组件B和组件C的重叠区域时,Stack A和组件C的onTouch事件会触发,组件C的点击事件会触发,组件B的onTouch事件和点击手势均不触发。
而当组件C设置hitTestBehavior为HitTestMode.Transparent时,点击组件B和组件C的重叠区域,组件A和组件C不受到影响与之前一致,组件A和组件C的onTouch事件会触发,组件C的点击手势会触发。而组件B因为组件C设置了HitTestMode.Transparent,组件B也收到了Touch事件,从而组件B的onTouch事件和点击手势触发。
ComponentA() {ComponentB().onTouch(() => {}).gesture(TapGesture({count: 1}))
}
.onTouch(() => {})
.gesture(TapGesture({count: 1}))
.hitTestBehavior(HitTestMode.None)
HitTestMode.None自身不响应触摸测试,不会阻塞子节点和兄弟节点的触摸控制。
当组件A未设置hitTestBehavior时,点击组件B区域时,组件A和组件B的onTouch事件均会触发,组件B的点击手势会触发。
当组件A设置hitTestBehavior为HitTestMode.None时,点击组件B区域时,组件B的onTouch事件触发,而组件A的onTouch事件无法触发,组件B的点击手势触发。
针对简单的场景,建议在单个组件上绑定hitTestBehavior。
针对复杂场景,建议在多个组件上绑定不同的hitTestBehavior来控制Touch事件的分发。
绑定收拾方法对手势的控制
设置绑定手势的方法可以实现在多层级场景下,当父组件与子组件绑定了相同的手势时,设置不同的绑定手势方法有不同的响应优先级。
当父组件使用.gesture绑定手势,父子组件所绑定手势类型相同时,子组件优先于父组件响应。
ComponentA() {ComponentB().gesture(TapGesture({count: 1}))
}
.gesture(TapGesture({count: 1}))
当父子组件均正常绑定点击手势时,子组件优先于父组件响应。
此时,单击组件B区域范围,组件B的点击手势会触发,组件A的点击手势不会触发。
如果以带优先级的方式绑定手势,则可使得父组件所绑定手势的响应优先级高于子组件。
ComponentA() {ComponentB().gesture(TapGesture({count: 1}))
}
.priorityGesture(TapGesture({count: 1}))
当父组件以.priorityGesture的形式绑定手势时,父组件所绑定的手势优先级高于子组件。
此时,单击组件B区域范围,组件A的点击手势会触发,组件B的点击手势不会触发。
如果需要父子组件所绑定的手势不发生冲突,均可响应,则可以使用并行的方式在父组件绑定手势。
ComponentA() {ComponentB().gesture(TapGesture({count: 1}))
}
.parallelGesture(TapGesture({count: 1}))
当父组件以.parallelGesture的形式绑定手势时,父组件和子组件所绑定的手势均可触发。
此时,单击组件B区域范围,组件A和组件B的点击手势均会触发。
1.2.5 手势拦截
手势拦截主要用于确保手势按需执行,有效解决手势冲突问题。典型应用场景包括:嵌套滚动、通过过滤组件响应手势的范围来优化交互体验。手势拦截主要采用手势触发控制和手势响应控制两种方法实现。
手势触发控制
手势触发控制是指,在系统判定阈值以满足的条件下,应用可自行判断是否拦截手势,使手势操作失败。
手势触发控制涉及以下接口。
接口 | 说明 |
---|---|
onGestureJudgeBegin | 用于手势拦截,是通用事件。在手势满足系统触发阈值场景下,回调给应用判断是否拦截手势。 |
onGestureRecognizerJudgeBegin | 用于手势拦截、获取手势识别器和初始化手势识别器开闭状态。是onGestureJudgeBegin接口的能力扩展,可以代替onGestureJudgeBegin接口。获取手势识别器时,会获取一次交互中手势响应链上的所有手势识别器,以及当前即将触发的手势识别器,初始化手势的激活状态。 |
以下示例中,Image和Stack两个组件位于同一区域。长按Stack组件的上半部分可触发挂载在Stack组件上的长按手势,长按Stack组件的下半部分则会响应Image组件的拖拽操作。
-
Image组件设置拖拽。
Image($r('sys.media.ohos_app_icon')).draggable(true).onDragStart(()=>{promptAction.showToast({ message: "Drag 下半区蓝色区域,Image响应" });}).width('200vp').height('200vp')
-
Stack组件设置手势。
Stack() {}.width('200vp').height('200vp').hitTestBehavior(HitTestMode.Transparent).gesture(GestureGroup(GestureMode.Parallel,LongPressGesture().onAction((event: GestureEvent) => {promptAction.showToast({ message: "LongPressGesture 长按上半区 红色区域,红色区域响应" });}).tag("longpress")))
-
Stack组件设置拦截。
.onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {// 如果是长按类型手势,判断点击的位置是否在上半区if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) {if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) {return GestureJudgeResult.CONTINUE;} else {return GestureJudgeResult.REJECT;}}return GestureJudgeResult.CONTINUE; })
-
代码完整示例。
import { promptAction } from '@kit.ArkUI';@Entry @Component struct Index {scroller: Scroller = new Scroller();build() {Scroll(this.scroller) {Column({ space: 8 }) {Text("包括上下两层组件,上层组件绑定长按手势,下层组件绑定拖拽。其中上层组件下半区域绑定手势拦截,使该区域响应下层拖拽手势。").width('100%').fontSize(20).fontColor('0xffdd00')Stack({ alignContent: Alignment.Center }) {Column() {// 模拟上半区和下半区Stack().width('200vp').height('100vp').backgroundColor(Color.Red)Stack().width('200vp').height('100vp').backgroundColor(Color.Blue)}.width('200vp').height('200vp')// Stack的下半区是绑定了拖动手势的图像区域。Image($r('sys.media.ohos_app_icon')).draggable(true).onDragStart(()=>{promptAction.showToast({ message: "Drag 下半区蓝色区域,Image响应" });}).width('200vp').height('200vp')// Stack的上半区是绑定了长按手势的浮动区域。Stack() {}.width('200vp').height('200vp').hitTestBehavior(HitTestMode.Transparent).gesture(GestureGroup(GestureMode.Parallel,LongPressGesture().onAction((event: GestureEvent) => {promptAction.showToast({ message: "LongPressGesture 长按上半区 红色区域,红色区域响应" });}).tag("longpress"))).onGestureJudgeBegin((gestureInfo: GestureInfo, event: BaseGestureEvent) => {// 如果是长按类型手势,判断点击的位置是否在上半区if (gestureInfo.type == GestureControl.GestureType.LONG_PRESS_GESTURE) {if (event.fingerList.length > 0 && event.fingerList[0].localY < 100) {return GestureJudgeResult.CONTINUE;} else {return GestureJudgeResult.REJECT;}}return GestureJudgeResult.CONTINUE;})}.width('100%')}.width('100%')}} }
手势响应控制
手势响应控制,指的是手势已经成功识别,但仍然可以通过调用API接口控制手势回调是否能够响应
手势响应控制的前提是手势识别成功,如果手势不成功也不会产生手势回调响应。
- 业务手势作业流:指真正触发UI变化的业务手势,比如使页面滚动的PanGesture,触发点击的TapGesture等。
- 监听手势作业流:指在监听手势运行的过程中,应根据上下文业务状态变化动态控制手势识别器的启停,例如在组件于嵌套坤东过程中是否已滑至边缘。这一监听事件可借助一个使用并行手势绑定的PanGesture实现,或者采用Touch事件来完成。
- 设置手势并行:此步骤并非必须,典型场景实在嵌套滚动中,设置外部组件的滚动收拾与内部的滚动手势并行。
- 动态开闭手势:指通过手势识别器的setEnable方法,控制手势是否响应用户回调。
手势响应控制涉及以下接口。
接口 | 说明 |
---|---|
shouldBuiltInRecognizerParallelWith | 用于设置系统组件内置手势与其他手势并行。 |
onGestureRecognizerJudgeBegin | 用于手势拦截,获取手势识别器,初始化手势识别器开闭状态。 |
parallelGesture | 可使开发者定义的手势,与比他优先级高的手势并行。 |
以下示例是两个Scroll组件的嵌套滚动场景,使用手势控制的api去控制外部组件和内部组件的嵌套滚动联动。
-
使用shouldBuiltInRecognizerParallelWith接口设置外部Scroll组件的PanGesture手势,与内部Scroll组件的PanGesture手势并行。
.shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => {for (let i = 0; i < others.length; i++) {let target = others[i].getEventTargetInfo();if (target.getId() == "inner" && others[i].isBuiltIn() && others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // 找到将要组成并行手势的识别器this.currentRecognizer = current; // 保存当前组件的识别器this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器return others[i]; // 返回和当前手势将要组成并行手势的识别器}}return undefined; })
-
使用onGestureRecognizerJudgeBegin接口获取到Scroll组件的PanGesture手势识别器,同时根据内外Scroll组件的边界条件,初始化内外手势的开闭状态。
.onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { // 在识别器即将要成功时,根据当前组件状态,设置识别器使能状态 let target = current.getEventTargetInfo();if (target.getId() == "outer" && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) {for (let i = 0; i < others.length; i++) {let target = others[i].getEventTargetInfo() as ScrollableTargetInfo;if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // 找到响应链上对应并行的识别器let panEvent = event as PanGestureEvent;this.childRecognizer.setEnabled(true);this.currentRecognizer.setEnabled(false);if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态if (panEvent && panEvent.offsetY < 0) {this.childRecognizer.setEnabled(false);this.currentRecognizer.setEnabled(true);}} else if (target.isBegin()) {if (panEvent.offsetY > 0) {this.childRecognizer.setEnabled(false);this.currentRecognizer.setEnabled(true);}}}}}return GestureJudgeResult.CONTINUE; })
-
设置监听手势,监听Scroll组件状态,动态调整手势开闭状态,以使手势响应。
.parallelGesture( // 绑定一个Pan手势作为动态控制器PanGesture().onActionUpdate((event: GestureEvent)=>{if (this.childRecognizer.getState() != GestureRecognizerState.SUCCESSFUL || this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制return;}let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo;let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo;if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) {this.childRecognizer.setEnabled(true);this.currentRecognizer.setEnabled(false);if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态if ((event.offsetY - this.lastOffset) < 0) {this.childRecognizer.setEnabled(false);if (currentTarget.isEnd()) {this.currentRecognizer.setEnabled(false);} else {this.currentRecognizer.setEnabled(true);}}} else if (target.isBegin()) {if ((event.offsetY - this.lastOffset) > 0) {this.childRecognizer.setEnabled(false);if (currentTarget.isBegin()) {this.currentRecognizer.setEnabled(false);} else {this.currentRecognizer.setEnabled(true);}}}}this.lastOffset = event.offsetY}) )
-
代码完整示例。
@Entry @Component struct FatherControlChild {scroller: Scroller = new Scroller();scroller2: Scroller = new Scroller();private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];private childRecognizer: GestureRecognizer = new GestureRecognizer();private currentRecognizer: GestureRecognizer = new GestureRecognizer();private lastOffset: number = 0;build() {Stack({ alignContent: Alignment.TopStart }) {Scroll(this.scroller) { // 外部滚动容器Column() {Text("Scroll Area").width('90%').height(150).backgroundColor(0xFFFFFF).borderRadius(15).fontSize(16).textAlign(TextAlign.Center).margin({ top: 10 })Scroll(this.scroller2) { // 内部滚动容器Column() {Text("Scroll Area2").width('90%').height(150).backgroundColor(0xFFFFFF).borderRadius(15).fontSize(16).textAlign(TextAlign.Center).margin({ top: 10 })Column() {ForEach(this.arr, (item: number) => {Text(item.toString()).width('90%').height(150).backgroundColor(0xFFFFFF).borderRadius(15).fontSize(16).textAlign(TextAlign.Center).margin({ top: 10 })}, (item: string) => item)}.width('100%')}}.id("inner").width('100%').height(800)}.width('100%')}.id("outer").height(600).scrollable(ScrollDirection.Vertical) // 滚动方向纵向.scrollBar(BarState.On) // 滚动条常驻显示.scrollBarColor(Color.Gray) // 滚动条颜色.scrollBarWidth(10) // 滚动条宽度.edgeEffect(EdgeEffect.None).shouldBuiltInRecognizerParallelWith((current: GestureRecognizer, others: Array<GestureRecognizer>) => {for (let i = 0; i < others.length; i++) {let target = others[i].getEventTargetInfo();if (target.getId() == "inner" && others[i].isBuiltIn() && others[i].getType() == GestureControl.GestureType.PAN_GESTURE) { // 找到将要组成并行手势的识别器this.currentRecognizer = current; // 保存当前组件的识别器this.childRecognizer = others[i]; // 保存将要组成并行手势的识别器return others[i]; // 返回和当前手势将要组成并行手势的识别器}}return undefined;}).onGestureRecognizerJudgeBegin((event: BaseGestureEvent, current: GestureRecognizer, others: Array<GestureRecognizer>) => { // 在识别器即将要成功时,根据当前组件状态,设置识别器使能状态 let target = current.getEventTargetInfo();if (target.getId() == "outer" && current.isBuiltIn() && current.getType() == GestureControl.GestureType.PAN_GESTURE) {for (let i = 0; i < others.length; i++) {let target = others[i].getEventTargetInfo() as ScrollableTargetInfo;if (target instanceof ScrollableTargetInfo && target.getId() == "inner") { // 找到响应链上对应并行的识别器let panEvent = event as PanGestureEvent;this.childRecognizer.setEnabled(true);this.currentRecognizer.setEnabled(false);if (target.isEnd()) { // 根据当前组件状态以及移动方向动态控制识别器使能状态if (panEvent && panEvent.offsetY < 0) {this.childRecognizer.setEnabled(false);this.currentRecognizer.setEnabled(true);}} else if (target.isBegin()) {if (panEvent.offsetY > 0) {this.childRecognizer.setEnabled(false);this.currentRecognizer.setEnabled(true);}}}}}return GestureJudgeResult.CONTINUE;}).parallelGesture( // 绑定一个Pan手势作为动态控制器PanGesture().onActionUpdate((event: GestureEvent)=>{if (this.childRecognizer.getState() != GestureRecognizerState.SUCCESSFUL || this.currentRecognizer.getState() != GestureRecognizerState.SUCCESSFUL) { // 如果识别器状态不是SUCCESSFUL,则不做控制return;}let target = this.childRecognizer.getEventTargetInfo() as ScrollableTargetInfo;let currentTarget = this.currentRecognizer.getEventTargetInfo() as ScrollableTargetInfo;if (target instanceof ScrollableTargetInfo && currentTarget instanceof ScrollableTargetInfo) {this.childRecognizer.setEnabled(true);this.currentRecognizer.setEnabled(false);if (target.isEnd()) { // 在移动过程中实时根据当前组件状态,控制识别器的开闭状态if ((event.offsetY - this.lastOffset) < 0) {this.childRecognizer.setEnabled(false);if (currentTarget.isEnd()) {this.currentRecognizer.setEnabled(false);} else {this.currentRecognizer.setEnabled(true);}}} else if (target.isBegin()) {if ((event.offsetY - this.lastOffset) > 0) {this.childRecognizer.setEnabled(false)if (currentTarget.isBegin()) {this.currentRecognizer.setEnabled(false);} else {this.currentRecognizer.setEnabled(true);}}}}this.lastOffset = event.offsetY;}))}.width('100%').height('100%').backgroundColor(0xDCDCDC)} }
二、手势事件冲突
在复杂的应用界面之中,多个组件嵌套时同时绑定手势事件,或者同一个组件同时绑定多个手势,都有可能导致手势事件产生冲突。
2.1 手势响应
根据事件的响应链收集,确定了响应链成员和事件响应的顺序。然而往往在处理一些业务时,需要给组件/不同组件添加更多二的手势和事件,比如onClick、API手势gesture等等,那么那个事件会得到响应呢?
2.1.1 手势响应优先级
手势按是否为系统内置手势,可以分为以下两类:
- 系统手势:系统控件默认实现的手势(系统内置手势),即调用某些通用事件内置的手势,比如拖拽、onClick;比如bindMenu内置的点击事件。
- 自定义手势:通过绑定手势API,例如使用gesture声明的事件回调,绑定长按手势事件回调。
除了触摸事件外的所有手势和事件,均是通过基础手势或者组合手势实现的。例如,拖拽事件是由长按手势和滑动手势组成的一个顺序手势。
在默认情况下,这些手势为非冒泡事件,当父组件和子组件绑定同类型的手势时,父子组件绑定的手势事件会发生竞争,子组件会优先识别绑定的手势
因此,除非显示声明允许多个手势同时成功,否则同一时间只有一个手势响应。
- 当父子组件均绑定同一类手势时,子组件优先于父组件的触发。
- 当同一个组件同时绑定多个手势时,先达到手势触发条件的手势优先触发。
- 当同一个组件绑定相同时间类型的系统手势和自定义手势时,系统手势会优先响应。
2.1.2 手势响应控制
上面介绍了手势默认的优先级顺序,在父子组件嵌套时,父子组件均绑定了手势或事件,或者同一个租价女同事绑定多个手势时,根据业务逻辑可能需要对手势是否需要响应、分发给谁响应、相应的顺序等做出控制。那么有哪些控制手段呢?
-
手势绑定
绑定手势方法
设置绑定手势的方法可以实现在多层级场景下,当父组件与子组件绑定了的相同的手势时,设置不同的绑定的手势方法有不同的响应优先级。手势绑定支持常规手势绑定方法(gesture)、带优先级手势绑定方法(priorityGesture)、并行手势绑定方法(parallelGesture)
绑定手势方法 功能规格 配参1 配参2 约束 gesture 绑定手势事件,父子组件交叠区域均绑定,响应子组件 GestureType GestureMask 与通用事件抢占 priorityGesture 当父组件配置priorityGesture时,优先识别父组件priorityGesture绑定的手势。 GestureType GestureMask 与通用事件抢占 parallelGesture 父组件绑定parallelGesture时,父子组件相同的手势事件都可以触发 GestureType GestureMask 无 前面讲到的手势的优先级是默认的,在加入了priorityGesture和parallelGesture绑定方法后,手势的响应顺序如下图所示:
不同手势绑定配参方案规格
父手势 子手势 GestureMask(父) 交叠区域相同事件响应方 交叠区域不同事件响应方 gesture gesture default 子组件 各自响应 gesture gesture IgnoreInternal 父组件 父组件 priorityGesture gesture default 父组件 各自响应 priorityGesture gesture IgnoreInternal 父组件 父组件 parallelGesture gesture default 各自响应 各自响应 parallelGesture gesture IgnoreInternal 父组件 父组件 组合手势
手势组合是指多种手势组合为复合手势,通过GestureGroup属性,可以给同一个组件添加多个手势,支持连续识别、并行识别和互斥识别模式。开发者可以根据业务需求,选择合适的组合模式。
接口 可选模式 描述 注册事件 GestureGroup Sequence 手势顺序队列,需要按预定的手势组顺序执行,有一个失败则全部失败 onCancel GestureGroup Parallel 手势组合,直到所有已识别的手势执行完 无 GestureGroup Exclusive 互斥识别,成功完成一个手势,则完成手势任务 无 -
独占事件控制
monopolizeEvents(monopolize: boolean)
通过monopolizeEvents属性设置组件是否独占事件,时间范围包括组件自带的事件和开发者自定义的点击、触摸、手势事件。先响应事件的控件作为第一响应者,在手指离开屏幕前其他组件不会响应任何事件。
在一个窗口内,设置了独占控制的组件上的事件如果首先相应,则本次交互只允许此组件上设置的事件响应,窗口内其他组件上的事件不会相应。
如果通过parallelGesture绑定了与子组件同时触发的手势,入PanGesture,子组件设置了独占资源切首个响应事件,则父组件手势不会相应。
-
自定义手势判定
onGestureJudgeBegin(callback: (gestureInfo: GestureInfo, event: BaseGestureEvent) => GestureJudgeResult): T
为组件提供自定义手势判定能力。可根据需要,在手势识别期间,根据自己的业务逻辑来决定是否响应手势。使用onGestureJudgeBegin方法对手势进行判定。
-
手势拦截增强
shouldBuiltInRecognizerParallelWith(callback: ShouldBuiltInRecognizerParallelWithCallback): T
为组件提供手势拦截能力,将系统内置手势和响应链上更高优先级的手势做并行化处理,并可以动态控制手势事件的触发。
-
responseRegion和hitTestBehavior
影响触摸测试的因素同样也可能会影响到手势的响应流程。例如responseRegion属性和hitTestBehavior属性可以控制Touch事件的分发,从而可以影响到onTouch事件和手势的响应。而绑定手势方法属性可以控制手势的竞争从而影响手势的响应,但不会影响到onTouch事件。
-
ArkUI组件自身的属性控制手势响应
ArkUI组件自身的属性,也可以对手势事件的响应做出控制。例如Grid、List、Scroll、Swiper、WaterFlow等滚动容器组件提供了nestedScroll属性,来解决和父组件的嵌套滚动的冲突问题;例如Swiper组件的disableSwipe可以设置禁用组件滑动切换的功能;又例如List组件可以通过设置enableScrollInteraction属性来设置是否支持手势滚动列表。
2.2 总结
手势冲突在界面开发中往往不可避免,特别是在复杂的应用界面中。针对不同的冲突场景和手势交互需求,需要选择合适的解决方案。可以参考前面介绍的影响触摸测试因素,以及手势响应控制里面的方法,进行尝试。
- Grid、List、Scroll、Swiper、WaterFlow等滚动容器的嵌套,可以尝试使用nestedScroll属性来解决视图滚动冲突的问题。
- 对于单个组件组合手势的使用产生的冲突,以及自定义手势和系统手势冲突,可以尝试使用组合手势中的顺序识别、并行识别和互斥识别来解决。
- 对于多层组件手势响应冲突,可以参考多层级手势事件。
- 如果需要将系统手势和比其优先级高的手势做并行化处理,并可以动态控制手势事件的触发,可以参考手势拦截增强。
- 如果只是需要动态控制自定义手势是否响应,可以参考自定义手势判定。
- 对于多点触控产生的手势冲突可以参考独占事件控制。
相关文章:
HarmonyOS ArkUI交互事件与手势处理全解析:从基础到高级实践
文章目录 一、交互事件1.1 通用事件1.1.1 事件分发1.1.1.1 触摸测试1. 触摸测试基本流程2. 触摸测试控制3. 自定义事件拦截4. 禁用控制5. 触摸热区设置6. 安全组件 1.1.1.2 事件响应链收集 1.1.2 触屏事件1.1.3 键鼠事件1.1.3.1 鼠标事件1.1.3.2 按键事件 1.1.4 焦点事件1.1.5 …...
【计算机网络】面试常考——GET 和 POST 的区别
GET 和 POST 的区别 GET 和 POST 是 HTTP 协议中最常用的两种请求方法,它们的主要区别体现在 用途、数据传输方式、安全性、缓存机制 等方面。以下是详细对比: 1. 用途 GET POST 主要用于 获取数据(如查询、搜索)。 主要用于 提…...
AI编程工具“幻觉”风险与飞算JavaAl的破局之道
近年来,AI编程辅助工具迅速崛起,极大地提升了开发者的工作效率。然而,这些工具普遍存在一个被称为“幻觉”(hallucination)的风险——AI可能会生成看似合理但实际错误、不安全或低效的代码。这种现象在复杂业务逻辑和特定领域开发中尤为明显&…...
【Python零基础入门系列】第1篇:Python 是什么?怎么装环境?推荐哪些 IDE?
各位网友们,欢迎来到我的 Python 学习专栏! 前两天看到新闻英伟达为 CUDA 添加原生 Python 支持,意味着开发者可直接用 Python 操作 GPU,加速 AI 和高性能计算,降低门槛,让 Python 的应用范围更广、能力更强。 一直想写一系列文章教知友们从零开始学会 Python 编程,目…...
VPN访问SAP组服务器报登陆负载均衡错误88:无法连接到消息服务器(RC=9)
用户反馈用SAPGUI接入SAP时报错:登陆负载均衡错误88:无法连接到消息服务器(RC9) 经了解是通过VPN访问,但VPN没有放行ICMP访问,导致不能PING通,不能确认是网络问题还是什么问题。 解决方案: 1、VPN由原&am…...
Linux查看程序端口占用情况
大家好,欢迎来到程序视点!我是你们的老朋友.小二! 核心问题: Tomcat 8080端口启动失败,提示端口被占用,但常规检查未发现Tomcat进程占用该端口。 关键排查步骤: 初步检查 使用 ps -aux | gre…...
[C]基础14.字符函数和字符串函数
博客主页:向不悔本篇专栏:[C]您的支持,是我的创作动力。 文章目录 0、总结1、字符分类、转换函数2、strlen的使用和模拟实现2.1 strlen的使用2.2 strlen的模拟实现 3、strcpy的使用和模拟实现3.1 strcpy的使用3.2 strcpy的模拟实现 4、strcat…...
三种机器学习类型
本文讲介绍三种机器学习类型:①监督学习,②无监督学习,③强化学习。我们主要了解监督学习和无监督学习即可。 下图介绍这三种机器学习类型的区别: 1 用来预测未来的监督学习 从有标签的训练数据中学习一个模型,用来…...
UE5 Set actor Location和 Set World Location 和 Set Relative Location 的区别
在 Unreal Engine 的蓝图里,SetRelativeLocation、SetWorldLocation 和 SetActorLocation 三个节点虽然都能改变物体位置,但作用对象和坐标空间(Coordinate Space)不同: 1. SetActorLocation 作用对象:整个…...
Glide 如何加载远程 Base64 图片
最近有个需求,后端给出的图片地址并不是正常的 URL,而且需要一个接口去请求,但是返回的是 base64 数据流。这里不关心为啥要这么多,原因有很多,可能是系统的问题,也可能是能力问题。当然作为我们 Android 程…...
JVM对象存储格式
引言 在Java虚拟机(JVM)中,对象的内存布局是一个重要的底层概念,它直接影响对象在内存中的存储方式和占用空间。了解对象存储格式不仅有助于优化程序性能,还能帮助我们更好地理解JVM的工作原理。本文将详细探讨对象存…...
3D Gaussian Splatting部分原理介绍和CUDA代码解读
本系列旨在帮助无CUDA代码经验的读者、以及3DGS的初学者理解代码逻辑。 3D GS论文原文链接:https://arxiv.org/abs/2308.04079 论文笔记链接:【论文笔记】3D Gaussian Splatting for Real-Time Radiance Field Rendering 【论文笔记】A Survey on 3D Ga…...
日本IT行业|salesforce开发语言占据的地位
在日本的IT行业中,Salesforce 开发语言处于一个较为专业但稳步增长的细分领域,并不是主流开发语言(如 Java、Python、PHP),但其在某些行业和场景中地位越来越重要。 本篇以下是详细分析: Salesforce开发语言…...
1.1 点云数据获取方式——引言
图1-1-1点云建筑场景图 点云数据是指能够描述外部场景、对象表面的三维空间位置,并具有相关属性的点集,其每个离散点通常包括三维空间位置(x,y,z)以及强度、颜色等属性信息。大量分布的离散点集能够清晰而直接地描绘场景、对象的3…...
接入层架构演变
1、单体架构 请求过程 浏览器的请求通过 DNS Server 解析到指定的 IP 地址,浏览器通过 IP 地址访问 Web Server 缺点 当到达 Web Server 的性能瓶颈时(瓶颈受到CPU,内存,io,带宽影响),无法进…...
python:sklearn 主成分分析(PCA)
参考书:《统计学习方法》第2版 第16章 主成分分析(PCA)示例 编写 test_pca_1.py 如下 # -*- coding: utf-8 -*- """ 主成分分析(PCA) """ import matplotlib.pyplot as plt from skl…...
力扣-数据结构-二叉树
94. 二叉树的中序遍历 给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。 示例 1: 输入:root [1,null,2,3] 输出:[1,3,2]示例 2: 输入:root [] 输出:[]示例 3: 输入&#x…...
嵌入式音视频实时通话EasyRTC打造设备安装与调试的高效远程解决方案
一、背景 在数字化浪潮席卷全球的今天,实时音视频通信技术已经成为众多领域不可或缺的重要组成部分。从智能家居到智能安防,从在线教育到远程医疗,人们对于高效、便捷、稳定且低延迟的音视频通信解决方案的需求日益迫切。而EasyRTC作为一款卓…...
AI 的未来是开源?DeepSeek 正在书写新篇章!
AI 的未来是开源?DeepSeek 正在书写新篇章! 随着人工智能(AI)技术的迅猛发展,越来越多的企业和研究机构开始关注 AI 的开放性和透明度。开源不仅能够促进技术创新,还能加速知识的传播和应用。在这个背景下…...
抢先体验全新极小大模型Qwen3:0.6B
全民都在期待DeepSeek-R2的发布,但是一不小心被阿里截胡了,2025 年 4 月 29 日,阿里巴巴发布并开源了通义千问 Qwen3 系列大模型。据 大模型镜像网站 上关于Qwen3的介绍: Qwen3 是 Qwen 系列中最新一代的大型语言模型,提供一整套密集模型和混合专家 (MoE) 模型。Qwen3 基…...
部署一个自己的Spring Ai 服务(deepseek/通义千问)
Spring Boot 无缝接入 DeepSeek 和通义千问请求日志记录及其ip黑白名单 SpringBoot版本 3.2.0 JDK 版本为17 redis 3.2.0 mybatis 3.0.3 依赖引入 关键依赖 <dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-openai-sp…...
第一讲 | 算法复杂度
算法复杂度 一、数据结构前言1、数据结构(DS)2、算法(Algorithm) 二、算法效率1、复杂度的概念 三、时间复杂度(1)、案例(2)、大O的渐进表示法(3)、时间复杂度…...
【运维】还原 Docker 启动命令的利器:runlike 与 docker-autocompose
🔍 还原 Docker 启动命令的利器:runlike 与 docker-autocompose 实用教程 在日常使用 Docker 时,我们常常通过 docker run 启动容器,但有时候过了一段时间就忘记了当初使用的具体参数(端口、挂载、环境变量等…...
IP属地是实时位置还是自己设置
刷微博、抖音时,评论区总能看到“IP属地”?这个突然冒出来的小标签,让不少网友摸不着头脑:IP属地是实时位置,还是可以自己设置?别急,今天咱们就来聊聊这个话题! 1、什么是IP属地…...
Android WIFI体系
先说说WifiLock、MulticastLock 、IWificond WifiLock 允许应用强制保持 WiFi 活跃,即便设备处于休眠状态。如WIFI_MODE_FULL_HIGH_PERF:保持高性能 WiFi 活跃状态,适用于高带宽需求,如视频通话、流媒体。经测试有的场景能减少10…...
什么是静态住宅ip,跨境电商为什么要用静态住宅ip
在数字时代,IP地址不仅是设备联网的“ID”,更是跨境电商运营中的关键工具。尤其对于需要长期稳定、安全操作的场景,静态住宅IP逐渐成为行业首选。 一、什么是静态住宅IP? 静态住宅IP(Static Residential IP࿰…...
常见位运算总结
目录 常见位运算总结 191:位1的个数 338:比特位计数 461:汉明距离 136:只出现一次的数字 260:只出现一次的数字III 常见位运算总结 191:位1的个数 链接:191. 位1的个数 - 力扣(LeetCode) class Sol…...
[密码学实战]SDF之对称运算类函数(四)
[密码学实战]SDF之对称运算类函数(四) 一、标准解读:GM/T 0018-2023核心要求 1.1 SDF接口定位 安全边界:硬件密码设备与应用系统间的标准交互层 功能范畴: #mermaid-svg-1jptduZFNFiRZ2lS {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16…...
【C++编程入门】:基本语法
上一篇提到了C关键字和缺省参数以及命名空间域,这篇继续分享C入门语法,把基本语法掌握扎实后面学习更才能更轻松一些。 目录 引用 引用的特性 常引用 内联函数 auto关键字 引用 引用不是新定义一个变量,而是给已存在变量取了一个别名&am…...
区块链最佳框架:Truffle vs Hardhat vs Brownie
区块链技术的快速发展使得智能合约开发成为主流,而选择合适的开发框架是提升效率的关键。目前,Truffle、Hardhat和Brownie是三大主流框架,它们各有特点,适用于不同的开发场景和开发者偏好。本文将从功能、生态系统、适用人群等角度…...
Apache Flink的架构设计与运行流程说明
在大数据领域,实时计算的重要性随着业务需求的爆发式增长愈发凸显。从电商的实时销量监控到金融的高频交易风控,从物联网设备的实时告警到社交平台的热点追踪,企业对“秒级甚至毫秒级”数据处理能力的需求已成为刚需。在众多实时计算框架中&a…...
AI+零售:智能推荐、无人店与供应链管理的未来
AI零售:智能推荐、无人店与供应链管理的未来 系统化学习人工智能网站(收藏):https://www.captainbed.cn/flu 文章目录 AI零售:智能推荐、无人店与供应链管理的未来摘要引言一、智能推荐系统:从流量收割到用…...
华为云IoT平台与MicroPython实战:从MQTT协议到物联网设备开发
目录 前言 1. 华为云 1.1. 创建实例 1.2. 创建产品 1.3. 编辑服务模型 1.4. 注册设备 1.4.1. 复制设备连接参数 1.5. 连接参考代码 2. micropython版-物联网 2.1. 环境搭建 2.2. 实现步骤 2.3. 示例代码 结语 前言 物联网(IoT)技术的快速发…...
【Linux】Linux内核模块开发
Linux内核模块开发 零、关于 1、概述 最近在学习Linux相关的东西,学习了U-Boot的编译,Linux的编译,能够在开发板上运行自己编译的U-Boot和Linux了,那么接下来就是在自己编译的Linux上做应用级或者系统级的开发了。本文以字符设…...
linux 下查看指定进程的内存CPU占用情况(用于程序崩溃类的排查)
在程序开发过程中,如果程序较为庞大,逻辑较为复杂时,容易出现运行时崩溃的问题。导致的原因有很多,我这里只对较为通用的内容占用情况作记录,如程序中对文件描述符打开未关闭(导致fd积攒过多超过了系统的标…...
ASP.NET MVC 入门指南五
26. 响应式设计与移动开发 26.1 响应式视图设计 为了使 MVC 应用程序在不同设备上都能提供良好的用户体验,需要采用响应式设计。可以使用 CSS 框架如 Bootstrap 来实现响应式布局。 引入 Bootstrap:在项目中引入 Bootstrap 的 CSS 和 JavaScript 文件。…...
字节跳动社招面经 —— BSP驱动工程师(4)
接前一篇文章:字节跳动社招面经 —— BSP驱动工程师(3) 本文内容参考: 嵌入式硬件平台修改启动地址-CSDN博客 特此致谢! 上一回开始针对于“嵌入式充电站”发的一篇文章字节跳动社招面经——BSP驱动工程师中的面试题…...
Spring MVC中自定义日期类型格式转换器
在Spring MVC中,自定义日期类型格式转换器可以通过实现Converter接口或使用DateTimeFormat注解。以下是两种方法的详细说明: 方法一:全局自定义转换器(推荐) 1. 创建日期转换器类 实现 org.springframework.core.con…...
【3D 地图】无人机测绘制作 3D 地图流程 ( 无人机采集数据 | 地图原始数据处理原理 | 数据处理软件 | 无人机测绘完整解决方案 )
文章目录 一、无人机采集数据1、多角度影像数据2、定位与姿态数据 二、无人机采集数据处理原理1、空三解算2、密集点云生成与三维重建3、地形与正射影像生成4、三维模型优化与瓦片化 三、无人机影像处理软件介绍 一、无人机采集数据 无人机原始数据采集 : 多角度影像数据 : 多…...
arduino Nano介绍
【仅供学习,具体参数参考官网或销售商】 Arduino Nano 是一款基于 ATmega328P 微控制器(或 ATmega168 旧版)的紧凑型开发板,专为嵌入式项目和原型设计而设计。 以下是Arduino Nano V3.0 328P详细介绍: 主要特性 微…...
解决 Flutter 在 iOS 真机上构建失败的问题
在开发 Flutter 应用时,有时会在尝试将应用部署到 iOS 真机时遇到构建失败的问题。错误信息通常类似于以下内容: Could not build the precompiled application for the device. Uncategorized (Xcode): Timed out waiting for all destinations matchi…...
【办公类-89-03】20250429AI写的研讨记录,清除格式,统一格式,名字替换。部分加粗,添加页眉
背景需求: 检查自即,需要AI一下院内的五次科研培训记录。 本次用了豆包 豆包写的不错,也是“水字数”的高手 把每次培训内容贴到WORD里 把AI资料贴到WORD里,发现问题: 1、字体、段落什么都是不统一的,需要统一改成宋体小四,1.5倍行距 2、十个研讨人也要改成真人。就找…...
react-native 安卓APK打包流程
一、使用keytool命令生成一个签名密钥 $ keytool -genkeypair -v -storetype PKCS12 -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 在 Windows 上keytool命令放在 JDK 的 bin 目录中(比如C:\Program Files\…...
Android Studio中OpenCV应用详解:图像处理、颜色对比与OCR识别
文章目录 一、OpenCV在Android中的集成与配置1.1 OpenCV简介1.2 在Android Studio中集成OpenCV1.2.1 通过Gradle依赖集成1.2.2 通过模块方式集成1.2.3 初始化OpenCV 1.3 OpenCV基础类介绍 二、指定区域图像抓取与对比2.1 图像抓取基础2.2 指定区域图像抓取实现2.2.1 从Bitmap中…...
企业办公协同平台安全一体化生态入住技术架构与接口标准分析报告
全球组织数字化与智能化背景下 企业办公协同平台安全一体化生态入住技术架构与接口标准分析报告 一、背景与市场需求 市场规模与增量 根据Statista数据,全球协同办公平台市场规模预计从2023年的$480亿增长至2027年的$900亿,年复合增长率(CAG…...
从零搭建体育比分网站:技术选型与API调用实战(附完整源码)
一、前言:为什么选择体育比分项目? 体育数据网站是练手全栈开发的绝佳项目,涉及: ✅ 前端(实时数据渲染、可视化图表) ✅ 后端(API对接、数据缓存、高并发优化) ✅ 数据库ÿ…...
非凸科技受邀出席AI SPARK活动,共探生成式AI驱动金融新生态
4月19日,由AI SPARK社区主办的“生成式AI创新与应用构建”主题沙龙在北京举行。活动聚焦生成式AI的技术突破与产业融合,围绕大模型优化、多模态应用、存内计算等前沿议题展开深度探讨。非凸科技受邀出席并发表主题演讲,深入解析金融垂直大模型…...
深入蜂窝物联网 第五章 EC-GSM-IoT 及其他技术:混合组网与前瞻
1. 前言与应用场景 在一些地区,GSM 网络仍然大面积覆盖且运营成本低廉,运营商可通过 EC-GSM-IoT(Extended Coverage GSM for IoT)在现有GSM基站上升级,实现物联网互联。同时,为了满足不同场景的需求,常常需要与 NB-IoT、LTE-M、5G RedCap 等技术混合组网,形成多层次、…...
2025年深圳软件开发公司推荐
随着移动互联网的深度发展,软件开发已成为企业实现数字化转型的重要途径。作为中国科技创新中心的深圳,汇聚了众多技术实力雄厚的软件开发企业。本文将为您精选推荐6家在深圳表现突出的软件开发服务商,帮助企业找到合适的数字化转型合作伙伴。…...
仿腾讯会议——注册登录UI
1、加载素材 2、新添加资源类 3、加载图片 4、添加左侧图片 在左侧添加一个标签 选择图片 选择图片 勾选保证图片不变形 5、修改组件名称 6、设置密码输入框 5、切换 6、编辑提示框 7、定义提交和清空的槽函数 8、设置页面标题和最先显示页面 9、清空登录信息函数实现 10、清空…...