标签 @Component 下的文章

写在前面

在万物互联的全场景时代,智能设备的形态正从手机、平板向车载、穿戴、智能家居等多端快速延伸,用户对交互体验的要求早已突破 “能用” 的基础阈值,转而追求 “自然、无感、高效” 的跨设备操作体验。手势交互作为一种摆脱物理按键束缚的自然交互方式,凭借其直观性和沉浸感,已成为 HarmonyOS 构建全场景生态的核心交互语言。HarmonyOS作为新一代面向万物互联的操作系统,不仅重构了多设备协同的底层逻辑,更在手势识别能力上完成了升级,它提供了一套轻量化、高适配的手势开发框架,让开发者仅通过几行代码就能实现丰富的手势交互,从而在不同设备上打造统一且流畅的用户体验。单一手势是 HarmonyOS 手势体系的基础单元,所有复杂的组合手势都由它演化而来。本文将从技术原理入手,系统拆解单一手势的类型、实现逻辑与设计原则,并结合真实场景代码案例,帮助开发者快速掌握这一核心能力,为打造全场景交互体验筑牢基础。

image.png

单一手势什么是?

在 HarmonyOS 的交互框架中,系统将通用输入事件划分为触屏、键鼠、焦点及拖拽等核心类型。而手势交互则是通过 “手势绑定方法 + 具体手势实例” 的组合来实现,根据交互复杂度又可细分为单一手势与组合手势两大类别。
单一手势是构成所有复杂交互的 “原子单元”,它仅通过单次、独立的触摸动作(如点击、滑动、按压)触发功能,是组合手势的基础组件,也是开发者入门 HarmonyOS 交互开发的第一课。

手势操作的类型及实现

关于单一手势操作的类型有点击、长按、拖动、捏合、旋转、滑动六大类型,具体实现如下所示。

1、点击手势(TapGesture):交互的起点

点击手势是最基本的手势操作,支持单次点击和多次点击。通过TapGesture可以轻松实现按钮点击、菜单打开等功能,以下代码展示了如何实现一个双击操作:

@Entry
@Component
struct Index {
  @State value: string = "";

  build() {
    Column() {
      Text('双击').fontSize(28)
        .gesture(
          TapGesture({ count: 2 }) // 绑定双击事件,绑定count为2的TapGesture
            .onAction((event: GestureEvent|undefined) => {
              this.value = JSON.stringify(event.fingerList[0]);
            }))
      Text(this.value)
    }
    .height(200)
    .width(250)
    .padding(20)
    .border({ width: 3 })
    .margin(30)
  }
}

2、长按手势(LongPressGesture):触发深度操作

长按手势常用于触发 “二次确认” 类功能,如长按复制、弹出操作菜单等,支持重复触发模式以实现连续交互。长按手势用于触发长按手势事件,通过LongPressGesture可以轻松实现长按复制等功能,以下代码展示了如何实现在Text组件上绑定可以重复触发的长按手势:

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

  build() {
    Column() {
      Text('长按').fontSize(28)
        .gesture(
          // 绑定可以重复触发的LongPressGesture
          LongPressGesture({ 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)
  }
}

3、拖动手势(PanGesture):实现元素自由位移

拖动手势在滑动距离超过系统阈值(默认 5vp)时触发,常用于实现组件拖拽、位置调整等交互,核心是通过回调实时更新布局参数。拖动手势用于触发拖动手势事件,以下代码展示了在Text组件上绑定拖动手势为例,可以通过在拖动手势的回调函数中修改组件的布局位置信息来实现组件的拖动:

@Entry
@Component
struct Index {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;

  build() {
    Column() {
      Text('拖动')
        .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) => {
            })
              // 当触发拖动手势时,根据回调函数修改组件的布局位置信息
            .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)
  }
}

4、捏合手势(PinchGesture):控制元素缩放

捏合手势通过识别多指(支持 2-5 指)的距离变化计算缩放比例,是图片浏览、地图缩放等场景的核心交互方式。捏合手势用于触发捏合手势事件,常用于图片和地图的缩放操作,这里举例以在Column组件上绑定三指捏合手势为例,可以通过在捏合手势的函数回调中获取缩放比例,实现对组件的缩小或放大:

@Entry
@Component
struct Index {
  @State scaleValue: number = 1;
  @State pinchValue: number = 1;
  @State pinchX: number = 0;
  @State pinchY: number = 0;

  build() {
    Column() {
      Column() {
        Text('捏合')
      }
      .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) => {
          })
            // 当捏合手势触发时,可以通过回调函数获取缩放比例,从而修改组件的缩放比例
          .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;
          })
      )
    }
  }
}

5、旋转手势(RotationGesture):实现元素角度调整

旋转手势通过识别触摸点的角度变化计算旋转值,常用于图片编辑、表盘调整等需要角度控制的场景。旋转手势用于触发旋转手势事件,这里以在Text组件上绑定旋转手势实现组件的旋转为例,可以通过在旋转手势的回调函数中获取旋转角度,从而实现组件的旋转:

@Entry
@Component
struct Index {
  @State angle: number = 0;
  @State rotateValue: number = 0;

  build() {
    Column() {
      Text('旋转').fontSize(28)
        // 在组件上绑定旋转布局,可以通过修改旋转角度来实现组件的旋转
        .rotate({ angle: this.angle })
        .gesture(
          RotationGesture()
           .onActionStart((event: GestureEvent|undefined) => {
            })
              // 当旋转手势生效时,通过旋转手势的回调函数获取旋转角度,从而修改组件的旋转角度
            .onActionUpdate((event: GestureEvent|undefined) => {
              if(event){
                this.angle = this.rotateValue + event.angle;
              }
            })
              // 当旋转结束抬手时,固定组件在旋转结束时的角度
            .onActionEnd(() => {
              this.rotateValue = this.angle;
            })
            .onActionCancel(() => {
            })
        )
        .height(200)
        .width(300)
        .padding(20)
        .border({ width: 3 })
        .margin(100)
    }
  }
}

6、滑动手势(SwipeGesture):触发页面级交互

滑动手势在滑动速度超过 100vp/s 时触发,支持上下左右四个方向,是列表滚动、页面切换等场景的核心交互。滑动手势用于触发滑动事件,可以实现上下左右滑动操作,常用于列表滚动和页面切换,以在Column组件上绑定滑动手势实现组件的旋转为例:

@Entry
@Component
struct Index {
  @State rotateAngle: number = 0;
  @State speed: number = 1;

  build() {
    Column() {
      Column() {
        Text('滑动')
      }
      .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、一致性

在整个HarmonyOS生态系统中,手势操作的含义和效果应保持一致,比如双指缩放手势在不同应用中都应实现放大或缩小的功能。

实战落地:单一手势在场景化开发中的典型应用

这里举两个简单的实用示例,方便各位学习使用。

1、音乐播放器应用

在音乐播放器应用中,用户可以通过左右滑动切换歌曲,双指缩放调整封面大小,以下代码展示了如何实现左右滑动切换歌曲:

@Entry
@Component
struct MusicPlayerSwipe {
  @State currentSongIndex: number = 0;
  @State songs: string[] = ['Song 1', 'Song 2', 'Song 3'];

  build() {
    Column() {
      Text(this.songs[this.currentSongIndex])
    }
    .swipe({
      start: SwipeDirection.Left,
      onSwipe: () => {
        if (this.currentSongIndex < this.songs.length - 1) {
          this.currentSongIndex++;
        }
      }
    })
    .swipe({
      start: SwipeDirection.Right,
      onSwipe: () => {
        if (this.currentSongIndex > 0) {
          this.currentSongIndex--;
        }
      }
    })
  }
}

2、手势截屏

手势截屏是另一个实用的功能,用户可以通过下滑手势调用全屏截图功能,通过双击手势调用区域截图功能,以下代码展示了如何实现全屏截图:

Stack() {
  Column() {
    ...
  }
  .gesture(
    PanGesture({
      fingers: 1,
      direction: PanDirection.Down,
      distance: CommonConstants.MINIMUM_FINGER_DISTANCE
    })
      .onActionStart(() => {
        let screenshotOptions: screenshot.ScreenshotOptions = {
          rotation: 0
        };
        screenshot.save(screenshotOptions, (err: Error, data: image.PixelMap) => {
          if (err) {
            Logger.error(`Failed to save the screenshot. Error:${JSON.stringify(err)}`);
          }
          if (this.pixelMap !== undefined) {
            this.pixelMap.release();
          }
          this.pixelMap = data;
          this.dialogController.open();
        });
      })
  )
}

最后

随着 HarmonyOS 6 的发布,全场景交互的边界正在被重新定义,单一手势作为交互体系的 “原子单元”,不仅是开发者构建基础交互的必备技能,更是打造复杂组合手势、实现多设备协同交互的核心基石。本文从技术原理、类型实现、设计原则到场景实战,系统拆解了 HarmonyOS 单一手势的开发逻辑。这些看似基础的交互能力,恰恰是构建 “自然、无感、高效” 全场景体验的关键:无论是手机端的流畅操作,还是车载、穿戴设备的极简交互,都离不开对单一手势的精准把控。对于 HarmonyOS 开发者而言,掌握单一手势的底层逻辑与实践技巧,不仅能提升应用的用户体验,更能为未来探索多设备协同手势、AI 增强手势等进阶能力筑牢基础。在万物互联的浪潮中,唯有以用户体验为核心,以技术实力为支撑,才能打造出真正适配全场景的优质应用。希望本文能成为你探索 HarmonyOS 交互开发的起点,在构建智能生态的道路上持续进阶。

在 ArkUI 里,除了 @State@Prop 这些状态/属性装饰器之外,还有一个很偏底层、但非常好用的能力:@Env 环境变量装饰器。

它的作用可以简单理解为:

把系统/运行环境的一些“全局状态”,以属性的形式注入到组件里,让 UI 能“感知环境变化”。

这篇文章就带你从 0 上手 @Env,并给出一个可直接改造进项目的示例。


一、@Env 是什么?能做什么?

官方定义:

  • 模块从 API Version 22 开始支持;
  • 支持元服务(Meta Service)使用;
  • 需要系统能力:SystemCapability.ArkUI.ArkUI.Full
  • 核心能力:提供 Env 这个装饰器,用来把系统环境变量注入 ArkUI 组件字段

基础用法长这样:

import { uiObserver } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @Env(SystemProperties.BREAK_POINT)
  breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;

  build() {
    // 根据 breakpoint 做自适应布局
  }
}

这里有三件事:

  1. 使用 @Env(...) 装饰组件字段;
  2. 参数是一个 SystemProperties 枚举值(环境变量的“key”);
  3. 装饰后的字段类型由这个环境变量决定,比如 BREAK_POINT 对应 WindowSizeLayoutBreakpointInfo
✅ 重点:当 @Env 写在 @Component / @ComponentV2 内部字段上时,它能拿到当前窗口的一些环境信息,而不是全局单例。

二、核心类型:EnvDecorator & SystemProperties

2.1 EnvDecorator 类型定义

declare type EnvDecorator = (value: SystemProperties) => PropertyDecorator;

也就是说:

  • Env 自己就是一个函数;
  • 它接受一个枚举值 SystemProperties
  • 返回一个 PropertyDecorator,用于修饰组件字段。

你平时用到的就是这个形式:

@Env(SystemProperties.BREAK_POINT)
breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;

2.2 SystemProperties 枚举

当前文档里只暴露了一个枚举值:

enum SystemProperties {
  BREAK_POINT = 'system.arkui.breakpoint'
}

说明:

  • BREAK_POINT:通过 @Env(SystemProperties.BREAK_POINT) 能获取到一个
    uiObserver.WindowSizeLayoutBreakpointInfo 实例;
  • 当装饰器声明在 @Component / @ComponentV2 里时,用来获取当前自定义组件所在窗口尺寸布局断点信息

简单理解:

这个 breakpoint 可以用来做「手机/平板/大屏」之类的响应式 UI 控制逻辑。

三、错误码:140000 如何排查?

@Env 只有一个官方错误码,非常好记:

错误码 ID错误信息含义
140000Invalid key for @Env传给 @Env(...) 的 key 不合法(不是支持的 SystemProperties

常见触发方式:

// ❌ 错误示例:写了不存在的 key
@Env('system.arkui.xx' as any)
env: any;

排查建议:

  1. 一定要使用 SystemProperties 枚举,不要手写字符串:

    @Env(SystemProperties.BREAK_POINT)
    breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;
  2. 确认当前 SDK / API Level 是否已经 ≥ 22
  3. 检查是不是写错了导入,或自定义了同名枚举覆盖了系统的 SystemProperties

四、最小可运行示例:打印窗口断点信息

先来一个最简单的 Demo:把断点信息打印出来,方便你在真机/模拟器上看效果。

import { uiObserver } from '@kit.ArkUI';

@Entry
@Component
struct BreakpointDemo {
  @Env(SystemProperties.BREAK_POINT)
  breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;

  build() {
    Column() {
      Text('当前窗口断点信息:')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 8 })

      // 简单直接:把对象序列化出来看
      Text(JSON.stringify(this.breakpoint))
        .fontSize(14)
        .fontColor('#999999')
        .lineHeight(18)
        .textAlign(TextAlign.Start)
        .margin({ left: 12, right: 12 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

建议你:

  • 在手机、平板、大屏或者调整窗口大小时多试试;
  • 观察 JSON.stringify(this.breakpoint) 输出的字段结构;
  • 再根据实际字段来写你的业务判断(比如宽度区间、layout 类型等)。
⚠️ 注意:WindowSizeLayoutBreakpointInfo 的字段以当前 SDK 官方文档为准,这里用 JSON.stringify 的方式,就是为了避免你一开始就被字段名卡住。

五、实战:用 @Env 写一个响应式布局

下面是一个「手机一列、大屏两列」的简化示例。重点是思路,你可以根据实际字段名调整判断逻辑。

5.1 思路设计

  1. @Env(SystemProperties.BREAK_POINT) 拿到断点信息;
  2. 根据断点信息判断当前属于 COMPACT / MEDIUM / EXPANDED 之类的类别(具体枚举以 SDK 为准);
  3. 用一个 getter 或方法,将断点映射到“列数”、“间距”等 UI 参数;
  4. build() 里根据这些参数布局内容。

5.2 示例代码(判断逻辑示意)

import { uiObserver } from '@kit.ArkUI';

@Entry
@Component
struct ResponsiveGridPage {
  @Env(SystemProperties.BREAK_POINT)
  breakpoint: uiObserver.WindowSizeLayoutBreakpointInfo;

  // 根据断点信息,推导当前列数(伪代码,具体判断按实际字段改)
  private get columnCount(): number {
    // 根据实际字段来写,比如 this.breakpoint.windowSizeClass / width / type 等等
    // 这里用伪逻辑举例:
    // - 小屏:1 列
    // - 中屏及以上:2 列
    // 请结合自己工程中的 WindowSizeLayoutBreakpointInfo 实际字段来判断
    try {
      // 你可以先打印 breakpoint 再决定判断方式
      return  this.isLargeLike() ? 2 : 1;
    } catch (e) {
      // 容错:拿不到断点时,降级为 1 列
      return 1;
    }
  }

  private isLargeLike(): boolean {
    // 这里仅示意:真实项目里用宽度、sizeClass 等字段来判断
    // 比如:
    // return this.breakpoint.width >= 600;
    console.info('breakpoint:', JSON.stringify(this.breakpoint));
    return false;
  }

  build() {
    Column() {
      Text('响应式布局示例(基于 @Env 断点)')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 12 })

      // 简单模拟一个“宫格列表”
      this.buildGrid()
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  private buildGrid() {
    // 为了示例简单,这里模拟 6 个 Item
    const items: number[] = [1, 2, 3, 4, 5, 6];

    if (this.columnCount === 1) {
      // 一列:竖向列表
      Column({ space: 8 }) {
        ForEach(items, (item: number) => {
          this.buildCard(item)
        })
      }
    } else {
      // 两列:简单两列栅格(更复杂的可以用自定义布局组件)
      Column({ space: 8 }) {
        ForEach(this.splitToRows(items, 2), (row: number[], index: number) => {
          Row({ space: 8 }) {
            ForEach(row, (item: number) => {
              // 每列占据一半空间
              this.buildCard(item)
                .layoutWeight(1)
            })
          }
        })
      }
    }
  }

  // 工具:把一维数组拆成二维
  private splitToRows(list: number[], count: number): number[][] {
    const result: number[][] = [];
    let temp: number[] = [];
    list.forEach((v, i) => {
      temp.push(v);
      if (temp.length === count || i === list.length - 1) {
        result.push(temp);
        temp = [];
      }
    });
    return result;
  }

  private buildCard(index: number) {
    return Column() {
      Text(`Card ${index}`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
      Text('这里是内容区域,可以放图片、标题、按钮等。')
        .fontSize(12)
        .fontColor('#999999')
        .margin({ top: 4 })
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 8, color: '#22000000', offsetY: 2 })
  }
}

上面例子里,有几点可以参考到自己的项目里:

  • @Env(...) 注入的环境变量封装成 getter/方法;
  • 组件内部只关心“几列”“间距多大”,而不关心“断点枚举”细节;
  • 后续要改断点规则,只用改 columnCount 的计算逻辑。

六、@Env 使用注意事项

6.1 只能装饰属性,且用在组件里才有意义

  • @Env 是装饰字段的,不是方法;
  • 建议用在 @Component / @ComponentV2 内部;
  • 如果你在普通类里用,通常是拿不到期望的 UI 环境(即使类型上不报错)。

6.2 环境变量是“只读语义”

虽然语法上你可以给字段重新赋值,但语义上 @Env 注入的是环境变量

  • 把它当“只读快照 + 自动更新”的数据源;
  • 不要指望在组件里 this.breakpoint = xxx 去修改系统状态。

6.3 响应性 & 性能

通常来说,@Env 注入的变量会随环境变化(比如窗口尺寸变更)而更新,你可以:

  • 直接在 build() 或 getter 里使用;
  • 如果需要更精细控制,可以配合自定义逻辑,在 aboutToAppear 中打印一次,了解变化频率,再做优化。

七、什么时候应该用 @Env?

可以简单记一个心法:

当你写 UI 时,发现需要「感知设备 /窗口环境」时,就可以想一想:能不能用 @Env?

典型场景包括:

  1. 响应式布局:

    • 不同断点展示不同列数、不同导航结构;
    • 小屏用 Tab,大屏用侧栏 + 内容区域。
  2. 窗口多实例 / 多窗口:

    • 同一个组件被复用到不同窗口中,需要根据各自窗口环境分别调整。
  3. 元服务 / 卡片场景:

    • 某些运行形态下环境信息不同,通过 @Env 拿到差异,裁剪 UI。

八、总结

@Env 看起来只是一个小小的装饰器,但定位其实很清晰:

  • @State / @Prop 管组件内部/外部数据;
  • @Env 管组件所处的“环境维度”的信息。

掌握它之后,你可以把「环境感知」这件事,从零散的 getWindowRect、全局单例逻辑中抽离出来,用更声明式、更 ArkUI 风格的写法来组织代码。