当手势遇见鸿蒙,交互体验竟能如此惊艳(2)

接上篇

单一手势

捏合手势(PinchGesture)

PinchGesture(value?:{fingers?:number, distance?:number})

捏合手势用于触发捏合手势事件

参数

  • fingers:用于声明触发捏合手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2。
  • distance:用于声明触发捏合手势的最小距离,单位为vp,默认值为5。

事件

示例

ts 复制代码
@Entry
@Component
struct PinchGesturePage {
  @State scaleValue: number = 1
  @State pinchValue: number = 1
  @State pinchX: number = 0
  @State pinchY: number = 0

  build() {
    Column() {
      Column() {
        Text('倍数scale: ' + this.scaleValue)
      }
      .height(200)
      .width(300)
      .padding(20)
      .border({ width: 3 })
      .justifyContent(FlexAlign.Center)
      .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
      //两指捏合触发该手势事件
      .gesture(
        PinchGesture({ fingers: 2 })
          .onActionStart((event: GestureEvent) => {
            console.info('Pinch start')
          })
          .onActionUpdate((event: GestureEvent) => {
            if (event) {
              this.scaleValue = this.pinchValue * event.scale
              this.pinchX = event.pinchCenterX
              this.pinchY = event.pinchCenterY
            }
          })
          .onActionEnd((event: GestureEvent) => {
            this.pinchValue = this.scaleValue
            console.info('Pinch end')
          })
      )
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

效果

PinchGesture与PinchGesture两种手势结合

我们将PinchGesture和PanGesture两种手势结合,实现在一定的空间中放大后,滑动放大画面。(最近我在项目遇到需要实现对直播流画面放大并滑动)

代码

ts 复制代码
@Entry
@Component
struct PinchGesturePage {
  @State scaleValue: number = 1
  @State pinchValue: number = 1
  @State pinchX: number = 0
  @State pinchY: number = 0
  @State zWidth: number = 300
  @State zHeight: number = 200
  //边界问题
  @State maxOffsetX: number = 0
  @State minOffsetX: number = 0
  @State maxOffsetY: number = 0
  @State minOffsetY: number = 0
  //滑动偏移量
  @State offsetX: number = 0
  @State offsetY: number = 0
  @State positionY: number = 0
  @State positionX: number = 0
  /**
   *触发边界检查
   */
  checkBoundary = () => {
    if (this.offsetX > this.maxOffsetX) {
      this.offsetX = this.maxOffsetX
    }
    if (this.offsetX < this.minOffsetX) {
      this.offsetX = this.minOffsetX
    }
    if (this.offsetY > this.maxOffsetY) {
      this.offsetY = this.maxOffsetY
    }
    if (this.offsetY < this.minOffsetY) {
      this.offsetY = this.minOffsetY
    }
  }

  build() {
    Column() {
      Column() {
      //这里需要自己选择一张图片
        Image($r('app.media.Q'))
          .height(this.zHeight)
          .width(this.zWidth)
          .objectFit(ImageFit.Fill)
          .scale({ x: this.scaleValue, y: this.scaleValue, z: 1 })
          .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
          .animation({ duration: 100, curve: Curve.Linear })//为了变化的过程更加流畅,我们选择增加一个动画效果
            //增加手势扩大子组件的画面,两指捏合触发该手势事件
          .gesture(
            PinchGesture({ fingers: 2 })
              .onActionStart((event: GestureEvent) => {
                console.info('Pinch start')
              })
              .onActionUpdate((event: GestureEvent) => {
                if (event) {
                  this.scaleValue = this.pinchValue * event.scale
                  //为了更好效果展示我们选择扩大
                  if (this.scaleValue <= 1) {
                    this.scaleValue = 1
                  }
                  this.pinchX = event.pinchCenterX
                  this.pinchY = event.pinchCenterY
                }
              })
              .onActionEnd((event: GestureEvent) => {
                this.pinchValue = this.scaleValue
                //在滑动过程中,我们需要处理画面边界问题,使用我们需要存储最大和最小偏移量
                this.maxOffsetX = Math.abs(Math.min(((1 - this.scaleValue) * this.zWidth) / 2, 0))
                this.minOffsetX = -Math.abs(Math.max(((this.scaleValue - 1) * this.zWidth) / 2, 0))
                this.maxOffsetY = Math.abs(Math.min(((1 - this.scaleValue) * this.zHeight) / 2, 0))
                this.minOffsetY = -Math.abs(Math.max(((this.scaleValue - 1) * this.zHeight) / 2, 0))
                //触发边界检查
                this.checkBoundary()
                console.info('Pinch end')
              })
          )
      }
      .height(200)
      .width(300)
      //clip属性防止子组件扩大之后画面超过父组件的范围
      .clip(true)
      .border({ width: 3 })
      .gesture(
        PanGesture({ fingers: 1 })
          .onActionStart((event) => {

          })
          .onActionUpdate((event: GestureEvent) => {
            if (event) {
              this.offsetX = this.positionX + event.offsetX
              this.offsetY = this.positionY + event.offsetY
              this.checkBoundary()
            }
          })
          .onActionEnd((event) => {
            if (event) {
              this.positionX = this.offsetX
              this.positionY = this.offsetY
            }
          })
      )
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

效果

旋转手势(RotationGesture)

RotationGesture(value?:{fingers?:number, angle?:number})

旋转手势用于触发旋转手势事件

参数

  • fingers:用于声明触发旋转手势所需要的最少手指数量,最小值为2,最大值为5,默认值为2
  • angle:用于声明触发旋转手势的最小改变度数,单位为deg,默认值为1。

事件

属性

示例

以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转:

ts 复制代码
@Entry
@Component
struct RotationGesturePage {
  @State angle: number = 0;
  @State rotateValue: number = 0;

  build() {
    Column() {
      Text('旋转角度' + 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)
        .border({ width: 3 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

效果

滑动手势(SwipeGesture)

SwipeGesture(value?:{fingers?:number, direction?:SwipeDirection, speed?:number})

滑动手势用于触发滑动事件,当滑动速度大于100vp/s时可以识别成功

注意:拖动手势(PanGesture)是滑动达到最小滑动距离(默认值为5vp)时拖动手势识别成功

冲突

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

参数

  • fingers:用于声明触发滑动手势所需要的最少手指数量,最小值为1,最大值为10,默认值为1。
  • direction:用于声明触发滑动手势的方向,此枚举值支持逻辑与(&)和逻辑或(|)运算。默认值为SwipeDirection.All。
  • speed:用于声明触发滑动的最小滑动识别速度,单位为vp/s,默认值为100。

事件

示例

ts 复制代码
@Entry
@Component
struct SwipeGesturePage {
  @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 })
      .animation({ duration: 100, curve: Curve.Linear })
      .gesture(
        // 绑定滑动手势且限制仅在竖直方向滑动时触发
        SwipeGesture({ direction: SwipeDirection.Vertical })// 当滑动手势触发时,获取滑动的速度和角度,实现对组件的布局参数的修改
          .onAction((event: GestureEvent | undefined) => {
            if (event) {
              this.speed = event.speed;
              this.rotateAngle = event.angle;
            }
          })
      )
    }
  }
}

效果

组合手势

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

GestureGroup(mode:GestureMode, gesture:GestureType[])

参数

  • mode:为GestureMode枚举类。用于声明该组合手势的类型。
  • gesture:由多个手势组合而成的数组。用于声明组合成该组合手势的各个手势。

顺序识别

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

示例

以一个由长按手势和拖动手势组合而成的连续手势为例:

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

ts 复制代码
@Entry
@Component
struct SequenceRecognition {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State count: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;
  @State borderStyles: BorderStyle = BorderStyle.Solid

  build() {
    Column() {
      Text('顺序识别\n' + '长按触发次数:' + this.count + '\n拖拽触发偏移量:\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组件上绑定点击手势和双击手势组成的并行识别手势为例,由于单击手势和双击手势是并行识别,因此两个手势可以同时进行识别,二者互不干涉。

说明
  • 当由单击手势和双击手势组成一个并行识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
  • 只有单次点击时,单击手势识别成功,双击手势识别失败。
  • 有两次点击时 ,若两次点击相距时间在规定时间内 (默认规定时间为300毫秒),触发两次单击事件和一次双击事件
  • 有两次点击时 ,若两次点击相距时间超出规定时间 ,触发两次单击事件不触发双击事件
ts 复制代码
@Entry
@Component
struct parallelRecognition {
  @State count1: number = 0;
  @State count2: number = 0;

  build() {
    Column() {
      Column() {
        Text('并行识别\n' + '单击次数:' + this.count1 + '\n双击次数:' + this.count2 + '\n')
          .fontSize(28)
      }
      .border({ width: 3 })
      .justifyContent(FlexAlign.Center)
      .height(200)
      .width(300)
      // 以下组合手势为并行并别,单击手势识别成功后,若在规定时间内再次点击,双击手势也会识别成功
      .gesture(
        GestureGroup(GestureMode.Parallel,
          TapGesture({ count: 1 })
            .onAction(() => {
              this.count1++;
            }),
          TapGesture({ count: 2 })
            .onAction(() => {
              this.count2++;
            })
        )
      )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)

  }
}

效果

互斥识别

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

示例

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

说明
  • 当由单击手势和双击手势组成一个互斥识别组合手势后,在区域内进行点击时,单击手势和双击手势将同时进行识别。
  • 当只有单次点击时,单击手势识别成功,双击手势识别失败。
  • 当有两次点击时,手势响应取决于绑定手势的顺序。若先绑定单击手势后绑定双击手势,单击手势在第一次点击时即宣告识别成功,此时双击手势已经失败。即使在规定时间内进行了第二次点击,双击手势事件也不会进行响应,此时会触发单击手势事件的第二次识别成功。若先绑定双击手势后绑定单击手势,则会响应双击手势不响应单击手势。
ts 复制代码
@Entry
@Component
struct exclusiveRecognition {
  @State count1: number = 0;
  @State count2: number = 0;

  build() {
    Column() {
      Column() {
        Text('并行识别\n' + '单击次数:' + this.count1 + '\n双击次数:' + this.count2 + '\n')
          .fontSize(28)
      }
      .height(200)
      .width('100%')
      .border({ width: 3 })
      .justifyContent(FlexAlign.Center)
      //以下组合手势为互斥并别,单击手势识别成功后,双击手势会识别失败
      .gesture(
        GestureGroup(GestureMode.Exclusive,
          TapGesture({ count: 1 })
            .onAction(() => {
              this.count1++;
            }),
          TapGesture({ count: 2 })
            .onAction(() => {
              this.count2++;
            })
        )
      )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)

  }
}

效果

结语

随着鸿蒙系统的持续迭代与优化,手势识别技术必将迎来更广阔的发展空间。未来,它或许能打破设备间的界限,在智能家居、智能车载等多元场景实现无缝衔接,为用户打造全场景、沉浸式的交互体验。而这,也激励着开发者不断探索,深挖手势识别技术的潜力,推动人机交互迈向新的台阶。如果您觉得这篇关于鸿蒙手势识别的分析对您有帮助,不妨点个赞,关注我。后续我会持续分享更多前沿技术动态,陪您一同探索科技的无限可能 。

相关推荐
别说我什么都不会11 分钟前
OpenHarmony解读之设备认证:sts协议-客户端发起start请求
物联网·嵌入式·harmonyos
聆听秋天的风11 分钟前
HarmonyOS NEXT练习:跑马灯
harmonyos
二流小码农24 分钟前
鸿蒙开发:使用Circle绘制圆形
android·ios·harmonyos
东林知识库44 分钟前
鸿蒙NEXT小游戏开发:垃圾分类
harmonyos
东林知识库44 分钟前
鸿蒙NEXT小游戏开发:区字棋
harmonyos
余多多_zZ1 小时前
HarmonyOSNext_API16_媒体查询
笔记·学习·华为·harmonyos·媒体
东林知识库1 小时前
鸿蒙NEXT小游戏开发:数字华容道
harmonyos
东林知识库1 小时前
鸿蒙NEXT小游戏开发:推箱子
harmonyos
东林知识库1 小时前
鸿蒙NEXT小游戏开发:拼图
harmonyos
东林知识库1 小时前
鸿蒙NEXT小游戏开发:打地鼠
harmonyos