单一手势

接口概览

fingers用于声明触发对应手势操作的手指数量,最小值1,最大值10,默认值1;

repeat用于声明是否连续触发事件回调,默认值false;

duration用于声明触发长按所需的最短时间,默认值500ms;

拖动手势中,direction用于声明触发拖动手势的方向;

拖动手势中,distance用于声明触发拖动的最小拖动识别距离,默认5vp;

捏合手势中,distance用于声明触发捏合的最小识别距离,默认5vp;

滑动手势中,direction用于声明触发滑动手势的方向;

speed用于声明触发滑动的最小滑动识别速度;

组件如何绑定手势

点击事件(onClick)

单击作为常用的手势,可以方便地使用onClick接口实现。尽管被称为事件,它实际上是基本手势类型,等同于将count配置为1的TapGesture,即单击手势。

onClick与其他手势类型相同,也会参与命中测试、响应链收集等过程。可以使用干预手势处理机制对onClick的响应进行动态决策。

点击手势(TapGesture)

点击手势支持单次点击和多次点击,参数定义参考TapGesture

长按手势(LongPressGesture)

长按手势用于触发长按手势事件,参数定义参考LongPressGesture

滑动手势(PanGesture)

滑动手势用于触发滑动手势事件,滑动达到最小滑动距离(默认值为5vp)时滑动手势识别成功,参数定义参考PanGesture

说明

  • 大部分可滑动组件,如List、Grid、Scroll、Tab等组件是通过PanGesture实现滑动,在组件内部的子组件绑定滑动手势(PanGesture)或者滑动手势(SwipeGesture)会导致手势竞争。

  • 当在子组件绑定PanGesture时,在子组件区域进行滑动仅触发子组件的PanGesture。如果需要父组件响应,需要通过修改手势绑定方法或者子组件向父组件传递消息进行实现,或者通过修改父子组件的PanGesture参数distance使得滑动更灵敏。当子组件绑定SwipeGesture时,由于PanGesture和SwipeGesture触发条件不同,需要修改PanGesture和SwipeGesture的参数以达到所需效果。

  • 不合理的阈值设置会导致滑动不跟手(响应时延慢)的问题。

捏合手势(PinchGesture)

捏合手势用于触发捏合手势事件,参数定义参考PinchGesture

旋转手势(RotationGesture)

旋转手势用于触发旋转手势事件,参数定义参考RotationGesture

快滑手势(SwipeGesture)

快滑手势用于触发快滑事件,当滑动速度大于100vp/s时可以识别成功,参数定义参考SwipeGesture

说明

当SwipeGesture和PanGesture同时绑定时,若二者是以默认方式或者互斥方式进行绑定时,会发生竞争。SwipeGesture的触发条件为滑动速度达到100vp/s,PanGesture的触发条件为滑动距离达到5vp,先达到触发条件的手势触发。可以通过修改SwipeGesture和PanGesture的参数以达到不同的效果。

组合手势

组合手势由多种单一手势组合而成,通过在GestureGroup中使用不同的GestureMode来声明该组合手势的类型,支持顺序识别并行识别互斥识别三种类型。

接口说明:

GestureGroup(mode:GestureMode, gesture:GestureType[])
mode:为GestureMode枚举类。用于声明该组合手势的类型。顺序识别组合手势对应的GestureMode为Sequence,并行识别组合手势对应的GestureMode为Parallel,互斥识别组合手势对应的GestureMode为Exclusive。
gesture:由多个手势组合而成的数组。用于声明该组合手势的各个手势。

使用举例

顺序识别

顺序识别组合手势对应的GestureMode为Sequence。顺序识别组合手势将按照手势的注册顺序识别手势,直到所有的手势识别成功。当顺序识别组合手势中有一个手势识别失败时,后续手势识别均失败。顺序识别手势中仅有最后一个手势可以响应onActionEnd

以一个由长按手势和滑动手势组合而成的顺序识别手势为例:

在一个Column组件上绑定了translate属性,通过修改该属性可以设置组件的位置移动。然后在该组件上绑定LongPressGesturePanGesture组合而成的Sequence组合手势。当触发LongPressGesture时,更新显示的数字。当长按后进行拖动时,根据滑动手势的回调函数,实现组件的拖动。代码参考组合手势示例

说明

拖拽事件是一种典型的顺序识别组合手势事件,由长按手势事件和滑动手势事件组合而成。只有先长按达到长按手势事件预设置的时间后进行滑动才会触发拖拽事件。如果长按事件未达到或者长按后未进行滑动,拖拽事件均识别失败。

并行识别

并行识别组合手势对应的GestureMode为Parallel。并行识别组合手势中注册的手势将同时进行识别,直到所有手势识别结束。并行识别手势组合中的手势进行识别时互不影响。

以在一个Column组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。 代码参考组合手势示例

说明

  • 当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。

  • 当只有单次点击时,单击手势识别成功,双击手势识别失败。

  • 当有两次点击时,若两次点击相距时间在规定时间内(默认规定时间为300毫秒),触发两次单击事件和一次双击事件。

  • 当有两次点击时,若两次点击相距时间超出规定时间,触发两次单击事件不触发双击事件。

互斥识别

互斥识别组合手势对应的GestureMode为Exclusive。互斥识别组合手势中注册的手势将同时进行识别,若有一个手势识别成功,则结束手势识别,其他所有手势识别失败。

以在一个Column组件上绑定单击手势和双击手势组合而成的互斥识别组合手势为例。若先绑定单击手势后绑定双击手势,由于单击手势只需要一次点击即可触发而双击手势需要两次,每次的点击事件均被单击手势消费而不能积累成双击手势,所以双击手势无法触发。若先绑定双击手势后绑定单击手势,则触发双击手势不触发单击手势。 代码参考组合手势示例

说明

  • 当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。

  • 当只有单次点击时,单击手势识别成功,双击手势识别失败。

  • 当有两次点击时,手势响应取决于绑定手势的顺序。若先绑定单击手势后绑定双击手势,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。若先绑定双击手势后绑定单击手势,则会响应双击手势而不响应单击手势。

多层级手势事件

多层级手势事件指父子组件嵌套时,父子组件均绑定了手势或事件。在该场景下,手势或者事件的响应受到多个因素的影响,相互之间发生传递和竞争,容易出现预期外的响应。

本章主要介绍了多层级手势事件的默认响应顺序,以及如何通过设置相关属性影响多层级手势事件的响应顺序

默认多层级手势事件

触摸事件

触摸事件(onTouch事件)是所有手势组成的基础,有Down,Move,Up,Cancel四种。手势均由触摸事件组成,例如,点击为Down+Up,滑动为Down+一系列Move+Up。触摸事件具有最特殊性:

1.监听了onTouch事件的组件。在手指落下被触摸时均会收到onTouch事件的回调,被触摸受到触摸热区和触摸控制影响。

2.onTouch事件的回调是闭环的。若一个组件收到了手指Id为0的Down事件,后续也会收到手指Id为0的Move事件和Up事件。

3.onTouch事件的回调是一致的。若一个组件收到了手指Id为0的Down事件,但未收到手指Id为1的Down事件,则后续只会收到手指Id为0的touch事件,不会收到手指Id为1的后续touch事件。

对于一般的容器组件(例如:Column),父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件根据布局进行触发。

Column() {
  Column().id('ComponentB').onTouch(() => {})
  Column().id('ComponentC').onTouch(() => {})
}.id('ComponentA').onTouch(() => {})

组件B和组件C作为组件A的子组件,当触摸到组件B或者组件C时,组件A也会被触摸到。onTouch事件允许多个组件同时触发,因此,当触摸组件B时,会触发组件A和组件B的onTouch回调,不会触发组件C的onTouch回调。

当触摸组件C时,会触发组件A和组件C的onTouch回调,不触发组件B的回调。

特殊的容器组件,如Stack等组件,由于子组件之间存在着堆叠关系,子组件的布局也存在相互遮盖关系。

所以,父子组件之间onTouch事件能够同时触发,兄弟组件之间onTouch事件会存在遮盖关系。

Stack() {
  Column().id('ComponentB').onTouch(() => {})
  Column().id('ComponentC').onTouch(() => {})
}.id('Stack A').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.当一个组件绑定多个手势时,先达到手势触发条件的手势优先触发。

自定义控制的多层级手势事件

可以通过设置属性,控制默认的多层级手势事件竞争流程,更好地实现手势事件。

目前,responseRegion属性和hitTestBehavior属性可以控制Touch事件的分发,从而可以影响到onTouch事件和手势的响应。而绑定手势方法属性可以控制手势的竞争从而影响手势的响应,但不能影响到onTouch事件

responseRegion对手势和事件的控制

responseRegion属性可以实现组件的响应区域范围的变化。响应区域范围可以超出或者小于组件的布局范围

    Column() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
        .responseRegion([rect1, rect2, rect3])
    }
    .id('ComponentA')
    .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属性可以实现在复杂的多层级场景下,一些组件能够响应手势和事件,而一些组件不能响应手势和事件

    Column() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
    
      Column() {
        Column()
          .id('ComponentD')
          .onTouch(() => {})
          .gesture(TapGesture({count: 1}))
      }
      .id('ComponentC')
      .onTouch(() => {})
      .gesture(TapGesture({count: 1}))
      .hitTestBehavior(HitTestMode.Block)
    }
    .id('ComponentA')
    .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() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
    
      Column()
        .id('ComponentC')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
        .hitTestBehavior(HitTestMode.Transparent)
    }
    .id('Stack A')
    .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事件触发。

    Column() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
    }
    .id('ComponentA')
    .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的点击手势触发。

    Stack() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
      Column() {
        Column()
          .id('ComponentD')
          .onTouch(() => {})
          .gesture(TapGesture({count: 1}))
      }
      .id('ComponentC')
      .onTouch(() => {})
      .gesture(TapGesture({count: 1}))
      .hitTestBehavior(HitTestMode.BLOCK_HIERARCHY)
    }
    .id('Stack A')
    .onTouch(() => {})
    .gesture(TapGesture({count: 1}))

    从API version 20开始,HitTestMode.BLOCK_HIERARCHY自身和子节点响应触摸测试,阻止所有优先级较低的兄弟节点和父节点参与触摸测试。

    当组件C未设置hitTestBehavior时,点击组件B和组件D的重叠区域时,组件A,组件C和组件D的onTouch事件均会触发,组件D的点击手势会触发。

    当组件C设置hitTestBehavior为HitTestMode.BLOCK_HIERARCHY时,点击组件B和组件D的重叠区域时,组件C和组件D的onTouch事件触发,组件A和组件B的onTouch事件无法触发,组件D的点击手势会触发。

    Stack() {
      Column()
        .id('ComponentB')
        .onTouch(() => {})
        .gesture(TapGesture({count: 1}))
      Column() {
        Column()
          .id('ComponentD')
          .onTouch(() => {})
          .gesture(TapGesture({count: 1}))
      }
      .id('ComponentC')
      .onTouch(() => {})
      .gesture(TapGesture({count: 1}))
      .hitTestBehavior(HitTestMode.BLOCK_DESCENDANTS)
    }
    .id('Stack A')
    .onTouch(() => {})
    .gesture(TapGesture({count: 1}))

    从API version 20开始,HitTestMode.BLOCK_DESCENDANTS自身不响应触摸测试,并且所有的后代(孩子,孙子等)也不响应触摸测试,不会影响祖先节点的触摸测试。

    若组件C未设置hitTestBehavior,点击组件B和组件D的重叠区域时,组件A、组件C和组件D都会触发onTouch事件,同时组件D的点击手势也会被触发。

    当组件C设置hitTestBehavior为HitTestMode.BLOCK_DESCENDANTS时,点击组件B和组件D的重叠区域时,组件A和组件B的onTouch事件触发,组件C和组件D的onTouch事件无法触发,组件B的点击手势会触发。

    针对简单的场景,建议在单个组件上绑定hitTestBehavior

    针对复杂场景,建议在多个组件上绑定不同的hitTestBehavior来控制Touch事件的分发

    绑定手势方法对手势的控制

    设置绑定手势的方法可以实现在多层级场景下,当父组件与子组件绑定了相同的手势时,设置不同的绑定手势方法有不同的响应优先级

    当父组件使用.gesture绑定手势,父子组件所绑定手势类型相同时,子组件优先于父组件响应。

    Column() {
      Column()
        .id('ComponentB')
        .gesture(TapGesture({count: 1}))
    }
    .id('ComponentA')
    .gesture(TapGesture({count: 1}))

    当父子组件均正常绑定点击手势时,子组件优先于父组件响应。

    此时,单击组件B区域范围,组件B的点击手势会触发,组件A的点击手势不会触发。

    如果以带优先级的方式绑定手势,则可使得父组件所绑定手势的响应优先级高于子组件

    Column() {
      Column()
        .id('ComponentB')
        .gesture(TapGesture({count: 1}))
    }
    .id('ComponentA')
    .priorityGesture(TapGesture({count: 1}))

    当父组件以.priorityGesture的形式绑定手势时,父组件所绑定的手势优先级高于子组件。

    此时,单击组件B区域范围,组件A的点击手势会触发,组件B的点击手势不会触发。

    如果需要父子组件所绑定的手势不发生冲突,均可响应,则可以使用并行的方式在父组件绑定手势

    Column() {
      Column()
        .id('ComponentB')
        .gesture(TapGesture({count: 1}))
    }
    .id('ComponentA')
    .parallelGesture(TapGesture({count: 1}))

    当父组件以.parallelGesture的形式绑定手势时,父组件和子组件所绑定的手势均可触发。

    此时,单击组件B区域范围,组件A和组件B的点击手势均会触发。

    OverlayManager的事件透传

    OverlayManager事件机制,默认优先WrappedBuilder内组件先接收,不会向下传递

    若希望OverlayManager下方的页面也能感应到事件,可采用hitTestBehavior(HitTestMode.Transparent)来传递事件,参考以下伪代码。

    @Builder
    function builderOverlay(params: Params) {
      Component1().hitTestBehavior(HitTestMode.Transparent)
    }
    
    // ···
    
      aboutToAppear(): void {
        // ···
        let componentContent = new ComponentContent(
          this.context, wrapBuilder<[Params]>(builderOverlay),
          new Params(uiContext, {x:0, y: 100})
        );
        this.overlayManager.addComponentContent(componentContent, 0);
      }

    结合拖拽和捏合手势实现简易相册

    代码参考结合拖拽和捏合手势实现简易相册

    Logo

    作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

    更多推荐