项目演示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 引言

在移动应用开发领域,手势交互已成为提升用户体验的核心技术之一。旋转手势(RotationGesture)作为双指交互的重要组成部分,广泛应用于图片查看器、地图导航、3D模型操作、表盘控件等场景。随着HarmonyOS NEXT的发布,ArkTS语言为开发者提供了更加简洁高效的声明式UI开发体验,使得手势交互的实现变得更加直观和优雅。

本文将深入探讨鸿蒙原生ArkTS布局方式中的RotationGesture旋转手势布局,从API基础到高级应用,从理论解析到实战案例,全面覆盖旋转手势的开发要点。本文基于API Level 24,所有代码示例均经过实际测试验证。


2. HarmonyOS NEXT 与 ArkTS 概述

2.1 HarmonyOS NEXT 架构演进

HarmonyOS NEXT是华为推出的新一代分布式操作系统,相比之前的版本,它在架构上进行了重大升级:

  • 纯血鸿蒙内核:采用全新的鸿蒙内核,摆脱了对Linux内核的依赖,实现了真正的自主可控
  • 原生应用开发:支持纯ArkTS开发,不再需要Java/C++层,开发效率大幅提升
  • 分布式能力增强:进一步强化了多设备协同能力,支持更丰富的设备间交互
  • 性能优化:通过全新的渲染引擎和编译优化,应用运行更加流畅

2.2 ArkTS 语言特性

ArkTS是HarmonyOS NEXT的主力开发语言,它在TypeScript的基础上进行了扩展,增加了声明式UI、状态管理、生命周期管理等特性。

核心装饰器:

@Entry
@Component
struct MyComponent {
  @State count: number = 0;

  build() {
    Column() {
      Text(`计数: ${this.count}`)
        .fontSize(24)
        .onClick(() => {
          this.count++;
        })
    }
  }
}
装饰器 说明
@Entry 页面入口装饰器,标识该组件为页面根组件
@Component 组件装饰器,标识该结构体为UI组件
@State 状态管理装饰器,响应式数据变更自动触发UI更新

2.3 声明式UI 与 手势系统

HarmonyOS的声明式UI框架将手势交互作为组件的属性进行声明:

Component()
  .gesture(
    RotationGesture({})
      .onActionStart((event) => { /* 手势开始 */ })
      .onActionUpdate((event) => { /* 手势更新 */ })
      .onActionEnd(() => { /* 手势结束 */ })
  )

3. RotationGesture API 详解

3.1 RotationGesture 构造函数

RotationGesture用于识别双指旋转手势,其构造函数签名如下:

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

参数说明:

参数 类型 默认值 说明
fingers number 2 触发旋转手势所需的手指数量
angle number 0 最小识别角度(度)

示例:

RotationGesture({})
RotationGesture({ angle: 10 })
RotationGesture({ fingers: 2 })

3.2 回调方法体系

RotationGesture提供了完整的回调方法体系:

RotationGesture({})
  .onActionStart((event: GestureEvent) => {})
  .onActionUpdate((event: GestureEvent) => {})
  .onActionEnd(() => {})
  .onActionCancel(() => {})

4. GestureEvent 事件对象深度解析

4.1 事件属性总览

interface GestureEvent {
  readonly angle: number;          // 当前旋转角度
  readonly angleDelta: number;     // 角度增量
  readonly fingers: number;        // 触点数量
  readonly centerX: number;        // 中心点X坐标
  readonly centerY: number;        // 中心点Y坐标
  readonly timestamp: number;      // 时间戳
}

4.2 角度相关属性

angle 属性:

  • 从手势开始到当前时刻的累计旋转角度
  • 正值表示顺时针旋转,负值表示逆时针旋转

angleDelta 属性:

  • 从上一次事件到当前事件的角度变化量
  • 用于计算旋转速度

5. 旋转角度累积模式详解

5.1 直接赋值模式的问题

// 错误示例
.onActionUpdate((event: GestureEvent) => {
  this.currentAngle = event.angle;  // 每次手势都会从0开始
})

5.2 基准角度+相对偏移模式

@State currentAngle: number = 0;
@State startAngle: number = 0;

.gesture(
  RotationGesture({})
    .onActionStart((event: GestureEvent) => {
      this.startAngle = this.currentAngle;
    })
    .onActionUpdate((event: GestureEvent) => {
      this.currentAngle = this.startAngle + event.angle;
    })
)

5.3 角度归一化处理

normalizeAngle(angle: number): number {
  let normalized = angle % 360;
  if (normalized < 0) {
    normalized += 360;
  }
  return normalized;
}

6. 基础示例:双指旋转卡片

6.1 完整代码实现

@Entry
@Component
struct Index {
  @State currentAngle: number = 0;
  @State startAngle: number = 0;

  build() {
    Column() {
      Stack() {
        Column()
          .width(200)
          .height(200)
          .linearGradient({ colors: [['#FF6B6B', 0], ['#4ECDC4', 1]] })
          .borderRadius(20)
          .shadow({ radius: 10, color: '#00000033', offsetY: 5 })

        Text('↑')
          .fontSize(60)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .zIndex(1)

        Text('0°')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .position({ top: 10, left: '50%' })
          .translate({ x: -15 })

        Text('90°')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .position({ top: '50%', right: 10 })
          .translate({ y: -10 })

        Text('180°')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .position({ bottom: 10, left: '50%' })
          .translate({ x: -15 })

        Text('270°')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .position({ top: '50%', left: 10 })
          .translate({ y: -10 })
      }
      .width(200)
      .height(200)
      .rotate({ angle: this.currentAngle, centerX: '50%', centerY: '50%' })
      .gesture(
        RotationGesture({})
          .onActionStart((event: GestureEvent) => {
            this.startAngle = this.currentAngle;
          })
          .onActionUpdate((event: GestureEvent) => {
            this.currentAngle = this.startAngle + event.angle;
          })
          .onActionEnd(() => {})
      )

      Column() {
        Text('双指旋转手势演示')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')

        Text(`当前旋转角度: ${Math.round(this.currentAngle)}°`)
          .fontSize(20)
          .fontColor('#666666')

        Text('请使用双指在卡片上进行旋转操作')
          .fontSize(14)
          .fontColor('#999999')
      }
      .margin({ top: 40 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

6.2 运行效果

  • 页面中央显示一个200×200的渐变卡片
  • 卡片上有向上箭头和角度标识
  • 使用双指旋转,卡片实时跟随手势旋转
  • 下方文本实时显示当前旋转角度值

7. 进阶示例:组合手势(缩放+旋转)

7.1 GestureGroup 组合手势

GestureGroup(
  GestureMode.Parallel,
  RotationGesture({}),
  PinchGesture({})
)

GestureMode 手势模式:

模式 说明
GestureMode.Sequence 顺序模式
GestureMode.Parallel 并行模式
GestureMode.Exclusive 互斥模式

7.2 组合手势实现

@Entry
@Component
struct ImageViewer {
  @State currentAngle: number = 0;
  @State startAngle: number = 0;
  @State currentScale: number = 1;
  @State startScale: number = 1;

  build() {
    Column() {
      Image($rawfile('demo.jpg'))
        .width(300)
        .height(300)
        .objectFit(ImageFit.Contain)
        .rotate({ angle: this.currentAngle, centerX: '50%', centerY: '50%' })
        .scale({ x: this.currentScale, y: this.currentScale, centerX: '50%', centerY: '50%' })
        .gesture(
          GestureGroup(
            GestureMode.Parallel,
            RotationGesture({})
              .onActionStart((event: GestureEvent) => {
                this.startAngle = this.currentAngle;
              })
              .onActionUpdate((event: GestureEvent) => {
                this.currentAngle = this.startAngle + event.angle;
              }),
            PinchGesture({})
              .onActionStart((event: GestureEvent) => {
                this.startScale = this.currentScale;
              })
              .onActionUpdate((event: GestureEvent) => {
                this.currentScale = this.startScale * event.scale;
                this.currentScale = Math.max(0.5, Math.min(3, this.currentScale));
              })
          )
        )

      Column() {
        Text('图片查看器')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Text(`旋转角度: ${Math.round(this.currentAngle)}°`)
          .fontSize(16)

        Text(`缩放比例: ${this.currentScale.toFixed(2)}x`)
          .fontSize(16)
      }
      .margin({ top: 40 })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F5F5F5')
  }
}

8. 进阶示例:动画旋转与状态复位

8.1 animateTo 动画方法

animateTo({
  duration: 500,
  curve: Curve.EaseOut,
}, () => {
  this.currentAngle = 0;
});

动画曲线:

曲线 说明
Curve.Linear 线性动画
Curve.Ease 缓动动画
Curve.EaseIn 缓入动画
Curve.EaseOut 缓出动画

8.2 状态复位实现

@Entry
@Component
struct RotationReset {
  @State currentAngle: number = 0;
  @State startAngle: number = 0;

  resetRotation() {
    animateTo({
      duration: 800,
      curve: Curve.EaseOut,
    }, () => {
      this.currentAngle = 0;
    });
  }

  build() {
    Column() {
      Stack() {
        Column()
          .width(150)
          .height(150)
          .backgroundColor('#4ECDC4')
          .borderRadius(10)

        Text('复位')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width(150)
      .height(150)
      .rotate({ angle: this.currentAngle, centerX: '50%', centerY: '50%' })
      .gesture(
        RotationGesture({})
          .onActionStart((event: GestureEvent) => {
            this.startAngle = this.currentAngle;
          })
          .onActionUpdate((event: GestureEvent) => {
            this.currentAngle = this.startAngle + event.angle;
          })
      )

      Button('点击复位')
        .width(150)
        .height(40)
        .backgroundColor('#FF6B6B')
        .fontColor('#FFFFFF')
        .borderRadius(20)
        .margin({ top: 20 })
        .onClick(() => {
          this.resetRotation();
        })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

8.3 惯性旋转效果

@Entry
@Component
struct InertialRotation {
  @State currentAngle: number = 0;
  @State startAngle: number = 0;
  @State lastAngle: number = 0;
  @State velocity: number = 0;
  @State isAnimating: boolean = false;

  build() {
    Column() {
      Stack() {
        Column()
          .width(180)
          .height(180)
          .backgroundColor('#9B59B6')
          .borderRadius(90)

        Text('惯性')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width(180)
      .height(180)
      .rotate({ angle: this.currentAngle, centerX: '50%', centerY: '50%' })
      .gesture(
        RotationGesture({})
          .onActionStart((event: GestureEvent) => {
            this.startAngle = this.currentAngle;
            this.lastAngle = event.angle;
            this.isAnimating = false;
          })
          .onActionUpdate((event: GestureEvent) => {
            this.currentAngle = this.startAngle + event.angle;
            this.velocity = event.angle - this.lastAngle;
            this.lastAngle = event.angle;
          })
          .onActionEnd(() => {
            this.startInertialAnimation();
          })
      )
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }

  startInertialAnimation() {
    if (Math.abs(this.velocity) < 0.5) return;
    this.isAnimating = true;

    const animate = () => {
      if (!this.isAnimating) return;
      this.currentAngle += this.velocity;
      this.velocity *= 0.95;
      if (Math.abs(this.velocity) > 0.1) {
        setTimeout(animate, 16);
      } else {
        this.isAnimating = false;
      }
    };

    setTimeout(animate, 16);
  }
}

9. 实战案例:指南针组件

9.1 需求分析与设计

需求描述:

  • 实现一个指南针组件,显示当前方向
  • 支持手动旋转调整
  • 显示0-360度刻度
  • 指针指向北方

9.2 完整组件实现

@Entry
@Component
struct Compass {
  @State currentAngle: number = 0;
  @State startAngle: number = 0;

  normalizeAngle(angle: number): number {
    let normalized = angle % 360;
    if (normalized < 0) normalized += 360;
    return normalized;
  }

  build() {
    Column() {
      Stack() {
        Column()
          .width(280)
          .height(280)
          .borderRadius(140)
          .backgroundColor('#2C3E50')
          .border({ width: 4, color: '#34495E' })

        ForEach([0, 90, 180, 270], (degree: number) => {
          Text(degree === 0 ? 'N' : degree === 90 ? 'E' : degree === 180 ? 'S' : 'W')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .position({
              top: degree === 0 ? 10 : degree === 180 ? 'auto' : '50%',
              bottom: degree === 180 ? 10 : undefined,
              left: degree === 270 ? 15 : degree === 90 ? 'auto' : '50%',
              right: degree === 90 ? 15 : undefined,
            })
            .translate({
              x: degree === 0 || degree === 180 ? -10 : 0,
              y: degree === 90 || degree === 270 ? -10 : 0,
            })
        })

        ForEach([0, 30, 60, 120, 150, 210, 240, 300, 330], (degree: number) => {
          Column()
            .width(2)
            .height(degree % 90 === 0 ? 15 : 8)
            .backgroundColor('#ECF0F1')
            .position({ top: 0, left: '50%' })
            .translate({ x: -1 })
            .rotate({ angle: degree, centerX: '50%', centerY: 140 })
        })

        Column()
          .width(4)
          .height(80)
          .linearGradient({ colors: [['#E74C3C', 0], ['#C0392B', 1]] })
          .position({ top: 30, left: '50%' })
          .translate({ x: -2 })
          .borderRadius({ bottomLeft: 2, bottomRight: 2 })

        Column()
          .width(6)
          .height(40)
          .backgroundColor('#ECF0F1')
          .position({ bottom: 30, left: '50%' })
          .translate({ x: -3 })
          .borderRadius({ topLeft: 3, topRight: 3 })
      }
      .width(280)
      .height(280)
      .rotate({ angle: -this.currentAngle, centerX: '50%', centerY: '50%' })
      .gesture(
        RotationGesture({})
          .onActionStart((event: GestureEvent) => {
            this.startAngle = this.currentAngle;
          })
          .onActionUpdate((event: GestureEvent) => {
            this.currentAngle = this.normalizeAngle(this.startAngle + event.angle);
          })
      )

      Column() {
        Text('指南针')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')

        Text(`当前方向: ${Math.round(this.currentAngle)}°`)
          .fontSize(20)
          .fontColor('#34495E')
          .margin({ top: 10 })
      }
      .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#ECF0F1')
  }
}

10. rotate/transform 修饰符深入解析

10.1 rotate 修饰符

.rotate({
  angle: number,
  centerX: string | number,
  centerY: string | number,
})

示例:

.rotate({ angle: 45, centerX: '50%', centerY: '50%' })
.rotate({ angle: 45, centerX: 0, centerY: 0 })
.rotate({ angle: 45, centerX: '100%', centerY: '100%' })

10.2 transform 修饰符

.transform({
  rotate: number,
  scale: { x: number, y: number },
  translate: { x: number, y: number },
  skew: { x: number, y: number },
})

11. 性能优化技巧

11.1 状态管理优化

.onActionUpdate((event: GestureEvent) => {
  const newAngle = this.startAngle + event.angle;
  if (Math.abs(newAngle - this.currentAngle) > 0.1) {
    this.currentAngle = newAngle;
  }
})

11.2 渲染性能优化

Stack() {
  // ...
}
.renderGroup(true)

11.3 手势冲突处理

.gesture(
  RotationGesture({})
    .priority(100)
)

12. 常见陷阱与解决方案

12.1 角度跳变问题

onActionUpdate((event: GestureEvent) => {
  let newAngle = this.startAngle + event.angle;
  
  if (newAngle - this.currentAngle > 180) {
    newAngle -= 360;
  } else if (newAngle - this.currentAngle < -180) {
    newAngle += 360;
  }
  
  this.currentAngle = newAngle;
})

12.2 手势识别延迟

RotationGesture({ angle: 1 })
  .priority(200)

12.3 组件层级遮挡

Stack() {
  ComponentA().zIndex(1)
  ComponentB().zIndex(2)
}
.clip(false)

13. 总结与展望

本文深入探讨了鸿蒙原生ArkTS布局方式中的RotationGesture旋转手势布局,从基础API到高级应用,全面覆盖了旋转手势的开发要点。

核心要点回顾:

  1. RotationGesture API:掌握构造函数参数和回调方法体系
  2. 角度累积模式:使用startAngle + event.angle实现连续旋转
  3. 组合手势:通过GestureGroup实现多种手势的组合
  4. 动画效果:使用animateTo实现平滑的状态过渡
  5. 性能优化:注意状态管理和渲染性能的优化

API Level 24 RotationGesture 相关API参考:

API 说明
RotationGesture 旋转手势识别器
GestureEvent 手势事件对象
GestureGroup 手势组合器
GestureMode 手势组合模式
rotate() 旋转变换修饰符
transform() 组合变换修饰符
animateTo() 动画过渡方法
PinchGesture 缩放手势识别器

本文基于 HarmonyOS NEXT API Level 24 编写
示例代码已通过实际测试验证

Logo

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

更多推荐