HarmonyOS6 ArkUI 组件尺寸变化事件(onSizeChange)全面解析与实战演示

文章目录

    • [一、onSizeChange 核心概念](#一、onSizeChange 核心概念)
      • [1.1 onSizeChange 与 onAreaChange 的关系](#1.1 onSizeChange 与 onAreaChange 的关系)
      • [1.2 SizeOptions 接口字段](#1.2 SizeOptions 接口字段)
      • [1.3 接口签名](#1.3 接口签名)
    • 二、触发时机与使用限制
      • [2.1 触发时机](#2.1 触发时机)
    • 三、基础用法示例
      • [3.1 最简监听写法(官方示例同款)](#3.1 最简监听写法(官方示例同款))
      • [3.2 安全读取 SizeOptions 字段](#3.2 安全读取 SizeOptions 字段)
      • [3.3 onSizeChange 与 onAreaChange 同时挂载](#3.3 onSizeChange 与 onAreaChange 同时挂载)
    • 四、可运行完整示例:双事件并列对比面板
    • 五、代码结构详解
      • [5.1 State 状态分区设计](#5.1 State 状态分区设计)
      • [5.2 onSizeChange 回调逻辑要点](#5.2 onSizeChange 回调逻辑要点)
      • [5.3 双 Builder 复用](#5.3 双 Builder 复用)
    • 总结

一、onSizeChange 核心概念

1.1 onSizeChange 与 onAreaChange 的关系

onSizeChangeonAreaChange轻量子集,两者均可监听组件宽高变化,但侧重不同:

复制代码
onAreaChange 监听范围:
┌──────────────────────────────────────────┐
│  宽高变化(width / height)               │
│  相对父容器坐标变化(position.x / .y)     │
│  相对屏幕坐标变化(globalPosition.x / .y) │
└──────────────────────────────────────────┘

onSizeChange 监听范围:
┌──────────────────────────────────────────┐
│  宽高变化(width / height)  ← 仅此两项   │
└──────────────────────────────────────────┘
对比项 onAreaChange onSizeChange
API 版本 8+ 12+
回调参数 Area(宽高 + position + globalPosition) SizeOptions仅 width / height
触发条件 尺寸位置任意变化 仅尺寸(宽高)变化时触发
参数类型 Length(`string number` 联合类型)
典型用途 浮层定位、坐标计算、全量感知 纯尺寸响应式布局、轻量监听

1.2 SizeOptions 接口字段

typescript 复制代码
// SizeOptions 接口定义
interface SizeOptions {
  width?: number | string   // 组件当前布局宽度(vp)
  height?: number | string  // 组件当前布局高度(vp)
}
字段 类型 单位 说明
width `number string` vp
height `number string` vp

1.3 接口签名

typescript 复制代码
// API 12+,适用于所有通用组件
onSizeChange(event: SizeChangeCallback): T

// SizeChangeCallback 类型定义
type SizeChangeCallback = (oldValue: SizeOptions, newValue: SizeOptions) => void
参数 类型 说明
oldValue SizeOptions 尺寸变化的宽高信息
newValue SizeOptions 尺寸变化的宽高信息(当前最新值)
返回值 组件实例 T 支持链式调用

二、触发时机与使用限制

2.1 触发时机

复制代码
组件首次挂载渲染完成
        ↓
onSizeChange 首次触发(oldValue 通常为 { width: 0, height: 0 })

        ......用户交互 / 布局变化......

组件 width 或 height 发生实际变化
        ↓
onSizeChange 再次触发(oldValue = 上次 newValue)

(注意:仅位置变化、不涉及宽高时不触发)

会触发 onSizeChange 的场景:

  1. 组件首次渲染完成 (初始化触发一次,oldValue{ width: 0, height: 0 }
  2. Text / Image 等组件内容变化导致宽高改变
  3. 父容器尺寸变化,引起子组件宽高联动变化
  4. 屏幕旋转窗口大小调整
  5. 条件渲染切换后组件重新挂载

不会触发 onSizeChange 的场景(与 onAreaChange 的关键区别):

  • 组件仅发生位置移动position.x/y 变化),宽高不变时不触发

三、基础用法示例

3.1 最简监听写法(官方示例同款)

与官方示例保持一致的最简写法,点击 Text 追加内容触发宽高变化:

typescript 复制代码
@Entry
@Component
struct AreaExample {
  @State value: string = 'Text'
  @State sizeValue: string = ''

  build() {
    Column() {
      Text(this.value)
        .backgroundColor(Color.Green)
        .margin(30)
        .fontSize(20)
        .onClick(() => {
          this.value += 'Text'
        })
        .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
          console.info(
            `on size change, old=${JSON.stringify(oldValue)}, new=${JSON.stringify(newValue)}`
          )
          this.sizeValue = JSON.stringify(newValue)
        })
      Text('new area is:\n' + this.sizeValue).margin({ left: 30, right: 30 })
    }
    .width('100%').height('100%').margin({ top: 30 })
  }
}

控制台输出示例:

复制代码
on size change,
  old={"width":0,"height":0}
  new={"width":56.7,"height":27.3}

首次触发时 oldValue 为 {width:0, height:0} ,与 onAreaChange 一致,表示从无尺寸到首次布局完成的变化,属于正常现象。

3.2 安全读取 SizeOptions 字段

SizeOptions 的字段为可选类型(number | string | undefined),读取时应做防护:

typescript 复制代码
.onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
  // ✅ 正确:对 oldValue 做 undefined 防护,newValue 正常触发时一定有值
  const oldW = oldValue.width  !== undefined ? Number(oldValue.width).toFixed(1)  : '-'
  const oldH = oldValue.height !== undefined ? Number(oldValue.height).toFixed(1) : '-'
  const newW = Number(newValue.width).toFixed(1)
  const newH = Number(newValue.height).toFixed(1)

  console.info(`尺寸变化:${oldW}×${oldH} → ${newW}×${newH} vp`)
})

3.3 onSizeChange 与 onAreaChange 同时挂载

两个事件可链式叠加到同一组件,互不干扰:

typescript 复制代码
Text(this.value)
  .fontSize(20)
  .padding(12)
  // onAreaChange:感知宽高 + 坐标变化(API 8+)
  .onAreaChange((oldValue: Area, newValue: Area) => {
    console.info(`[onAreaChange] 宽高或位置变化`)
    console.info(`  old: w=${Number(oldValue.width).toFixed(1)} pos=(${Number(oldValue.position?.x ?? 0).toFixed(1)}, ${Number(oldValue.position?.y ?? 0).toFixed(1)})`)
    console.info(`  new: w=${Number(newValue.width).toFixed(1)} pos=(${Number(newValue.position?.x ?? 0).toFixed(1)}, ${Number(newValue.position?.y ?? 0).toFixed(1)})`)
  })
  // onSizeChange:仅感知宽高变化(API 12+)
  .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
    console.info(`[onSizeChange] 仅宽高变化`)
    console.info(`  old: ${JSON.stringify(oldValue)}`)
    console.info(`  new: ${JSON.stringify(newValue)}`)
  })

观察规律 :两者同时挂载时,每次尺寸变化会先触发 onAreaChange,后触发 onSizeChange;若仅位置移动(宽高不变),则只触发 onAreaChangeonSizeChange 不触发。


四、可运行完整示例:双事件并列对比面板

  1. 点击追加内容同时触发 onAreaChangeonSizeChange 的被监听 Text 组件
  2. 蓝色面板onAreaChange 左右双栏展示变化前后的 6 个字段(宽高 + position + globalPosition)
  3. 紫色面板onSizeChange 左右双栏展示变化前后的 2 个字段(仅宽高)
  4. 三格计数统计:点击次数 / onAreaChange 触发次数 / onSizeChange 触发次数
  5. 完整 JSON 日志同步输出到控制台
typescript 复制代码
@Entry
@Component
struct AreaExample {
  @State value: string = 'Text'
  @State clickCount: number = 0
  @State changeCount: number = 0
  @State sizeChangeCount: number = 0

  // onAreaChange 旧区域信息
  @State oldWidth: string = '-'
  @State oldHeight: string = '-'
  @State oldPosX: string = '-'
  @State oldPosY: string = '-'
  @State oldGlobalX: string = '-'
  @State oldGlobalY: string = '-'

  // onAreaChange 新区域信息
  @State newWidth: string = '-'
  @State newHeight: string = '-'
  @State newPosX: string = '-'
  @State newPosY: string = '-'
  @State newGlobalX: string = '-'
  @State newGlobalY: string = '-'

  // onSizeChange 尺寸信息
  @State sizeOldW: string = '-'
  @State sizeOldH: string = '-'
  @State sizeNewW: string = '-'
  @State sizeNewH: string = '-'

  build() {
    Column({ space: 16 }) {

      // ── 标题 ──────────────────────────────────────────
      Text('onAreaChange & onSizeChange')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 24, bottom: 4 })

      Text('点击绿色文字可追加内容,同时触发两个事件')
        .fontSize(12)
        .fontColor('#888888')

      // ── 被监听的文本组件 ──────────────────────────────
      Text(this.value)
        .backgroundColor(Color.Green)
        .fontColor(Color.White)
        .borderRadius(8)
        .padding(12)
        .fontSize(20)
        .margin(30)
        .onClick(() => {
          this.clickCount++
          this.value = this.value + 'Text'
        })
        // ── onAreaChange:同时感知尺寸 + 位置变化(API 8+)──
        .onAreaChange((oldValue: Area, newValue: Area) => {
          this.changeCount++

          // Area.width/height 类型为 Length(string|number),需先转 number
          this.oldWidth   = Number(oldValue.width).toFixed(1)
          this.oldHeight  = Number(oldValue.height).toFixed(1)
          this.oldPosX    = Number(oldValue.position?.x ?? 0).toFixed(1)
          this.oldPosY    = Number(oldValue.position?.y ?? 0).toFixed(1)
          this.oldGlobalX = Number(oldValue.globalPosition?.x ?? 0).toFixed(1)
          this.oldGlobalY = Number(oldValue.globalPosition?.y ?? 0).toFixed(1)

          this.newWidth   = Number(newValue.width).toFixed(1)
          this.newHeight  = Number(newValue.height).toFixed(1)
          this.newPosX    = Number(newValue.position?.x ?? 0).toFixed(1)
          this.newPosY    = Number(newValue.position?.y ?? 0).toFixed(1)
          this.newGlobalX = Number(newValue.globalPosition?.x ?? 0).toFixed(1)
          this.newGlobalY = Number(newValue.globalPosition?.y ?? 0).toFixed(1)

          console.info(
            `[onAreaChange #${this.changeCount}] ` +
            `old=${JSON.stringify(oldValue)} new=${JSON.stringify(newValue)}`
          )
        })
        // ── onSizeChange:仅感知宽高变化(API 12+)──────
        .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
          this.sizeChangeCount++

          // SizeOptions.width/height 类型为 number,可直接调用 toFixed
          this.sizeOldW = oldValue.width !== undefined ? Number(oldValue.width).toFixed(1) : '-'
          this.sizeOldH = oldValue.height !== undefined ? Number(oldValue.height).toFixed(1) : '-'
          this.sizeNewW = Number(newValue.width).toFixed(1)
          this.sizeNewH = Number(newValue.height).toFixed(1)

          console.info(
            `[onSizeChange #${this.sizeChangeCount}] ` +
            `old=${JSON.stringify(oldValue)} new=${JSON.stringify(newValue)}`
          )
        })

      // ── 计数统计行 ─────────────────────────────────────
      Row({ space: 24 }) {
        this.CountItem('点击', this.clickCount, '#555555')
        this.CountItem('onAreaChange', this.changeCount, '#2980b9')
        this.CountItem('onSizeChange', this.sizeChangeCount, '#8e44ad')
      }
      .justifyContent(FlexAlign.Center)

      // ── onAreaChange 对比面板 ──────────────────────────
      Column({ space: 6 }) {
        Text('onAreaChange  (宽高 + position + globalPosition)')
          .fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2980b9')
        Row({ space: 10 }) {
          // 旧区域
          Column({ space: 5 }) {
            Text('变化前 oldValue').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#e74c3c')
            this.AreaInfoItem('width',         this.oldWidth,   'vp')
            this.AreaInfoItem('height',        this.oldHeight,  'vp')
            this.AreaInfoItem('position.x',    this.oldPosX,    'vp')
            this.AreaInfoItem('position.y',    this.oldPosY,    'vp')
            this.AreaInfoItem('globalPos.x',   this.oldGlobalX, 'vp')
            this.AreaInfoItem('globalPos.y',   this.oldGlobalY, 'vp')
          }
          .layoutWeight(1).padding(10).backgroundColor('#fff5f5').borderRadius(8)

          // 新区域
          Column({ space: 5 }) {
            Text('变化后 newValue').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#27ae60')
            this.AreaInfoItem('width',         this.newWidth,   'vp')
            this.AreaInfoItem('height',        this.newHeight,  'vp')
            this.AreaInfoItem('position.x',    this.newPosX,    'vp')
            this.AreaInfoItem('position.y',    this.newPosY,    'vp')
            this.AreaInfoItem('globalPos.x',   this.newGlobalX, 'vp')
            this.AreaInfoItem('globalPos.y',   this.newGlobalY, 'vp')
          }
          .layoutWeight(1).padding(10).backgroundColor('#f0fff4').borderRadius(8)
        }
      }
      .width('100%').padding({ left: 16, right: 16 })

      // ── onSizeChange 对比面板 ──────────────────────────
      Column({ space: 6 }) {
        Text('onSizeChange  (仅宽高,API 12+)')
          .fontSize(13).fontWeight(FontWeight.Bold).fontColor('#8e44ad')
        Row({ space: 10 }) {
          Column({ space: 5 }) {
            Text('变化前 oldValue').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#e74c3c')
            this.AreaInfoItem('width',  this.sizeOldW, 'vp')
            this.AreaInfoItem('height', this.sizeOldH, 'vp')
          }
          .layoutWeight(1).padding(10).backgroundColor('#fdf2f8').borderRadius(8)

          Column({ space: 5 }) {
            Text('变化后 newValue').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#27ae60')
            this.AreaInfoItem('width',  this.sizeNewW, 'vp')
            this.AreaInfoItem('height', this.sizeNewH, 'vp')
          }
          .layoutWeight(1).padding(10).backgroundColor('#f5eef8').borderRadius(8)
        }
      }
      .width('100%').padding({ left: 16, right: 16 })

      // ── 使用说明 ───────────────────────────────────────
      Column({ space: 5 }) {
        Text('使用说明').fontSize(13).fontWeight(FontWeight.Bold)
        Text('• onAreaChange:宽高 + 位置同时变化时触发(API 8+)')
          .fontSize(12).fontColor('#555555')
        Text('• onSizeChange:仅宽高变化时触发,不含坐标(API 12+)')
          .fontSize(12).fontColor('#555555')
        Text('• position 为相对父容器坐标,globalPosition 为相对屏幕坐标')
          .fontSize(12).fontColor('#555555')
        Text('• 控制台可查看完整 JSON 日志')
          .fontSize(12).fontColor('#555555')
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('#f8f8f8')
      .borderRadius(8)
      .margin({ left: 16, right: 16, bottom: 16 })

    }
    .width('100%')
    .height('100%')
    .margin({ top: 8 })
  }

  // ── 计数卡片构建器 ─────────────────────────────────────
  @Builder
  CountItem(label: string, count: number, color: string) {
    Column({ space: 2 }) {
      Text(`${count}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor(color)
      Text(label).fontSize(10).fontColor('#999999')
    }
  }

  // ── 信息行构建器 ───────────────────────────────────────
  @Builder
  AreaInfoItem(label: string, value: string, unit: string) {
    Row() {
      Text(label).fontSize(11).fontColor('#666666').layoutWeight(1)
      Text(`${value} ${unit}`).fontSize(11).fontWeight(FontWeight.Medium)
    }
  }
}

图:运行效果示意------点击绿色文字追加内容,蓝色面板显示 onAreaChange 的 6 个字段,紫色面板显示 onSizeChange 的 2 个字段,顶部三格计数器实时对比两个事件的触发次数


五、代码结构详解

5.1 State 状态分区设计

示例将 onAreaChangeonSizeChange 的状态完全分区,互不干扰:

typescript 复制代码
// onAreaChange 专用状态(12 个字段:oldValue/newValue 各 6 个)
@State oldWidth: string = '-'    // 变化前宽度
@State oldGlobalX: string = '-'  // 变化前全局 X 坐标
// ...共 12 个

// onSizeChange 专用状态(4 个字段:oldValue/newValue 各 2 个)
@State sizeOldW: string = '-'    // 变化前宽度
@State sizeOldH: string = '-'    // 变化前高度
@State sizeNewW: string = '-'    // 变化后宽度
@State sizeNewH: string = '-'    // 变化后高度

5.2 onSizeChange 回调逻辑要点

typescript 复制代码
.onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
  this.sizeChangeCount++

  // 关键 1:oldValue 字段为可选类型(undefined 防护)
  // 首次触发时 oldValue = { width: 0, height: 0 },通常有值但需防御
  this.sizeOldW = oldValue.width !== undefined
    ? Number(oldValue.width).toFixed(1)
    : '-'

  // 关键 2:newValue 在正常触发时一定有值,可直接转换
  this.sizeNewW = Number(newValue.width).toFixed(1)
  this.sizeNewH = Number(newValue.height).toFixed(1)

  // 关键 3:完整 JSON 日志,便于与 onAreaChange 日志对照
  console.info(
    `[onSizeChange #${this.sizeChangeCount}] ` +
    `old=${JSON.stringify(oldValue)} new=${JSON.stringify(newValue)}`
  )
})

5.3 双 Builder 复用

typescript 复制代码
// CountItem:三格计数统计,接收 label/count/color 参数,统一渲染
@Builder
CountItem(label: string, count: number, color: string) {
  Column({ space: 2 }) {
    Text(`${count}`).fontSize(20).fontWeight(FontWeight.Bold).fontColor(color)
    Text(label).fontSize(10).fontColor('#999999')
  }
}

// AreaInfoItem:单行信息项,复用于 onAreaChange(6行×2列)和 onSizeChange(2行×2列)
@Builder
AreaInfoItem(label: string, value: string, unit: string) {
  Row() {
    Text(label).fontSize(11).fontColor('#666666').layoutWeight(1)
    Text(`${value} ${unit}`).fontSize(11).fontWeight(FontWeight.Medium)
  }
}


总结

本文系统讲解了 HarmonyOS ArkUI 组件尺寸变化事件 的完整知识体系,核心要点回顾:

  1. onSizeChangeonAreaChange 的轻量子集 :只关注宽高变化,不含坐标信息,API 12+ 支持;选型原则是"只需宽高用 onSizeChange,还需坐标用 onAreaChange"
  2. 双快照回调oldValue(变化前)和 newValue(变化后)同时提供,无需手动缓存上次宽高值,可直接计算变化增量
  3. oldValue 字段为可选类型 :处理 oldValue.width/height 时需做 undefined 防护(?? 0!== undefined 判断),newValue 在正常触发时一定有值
  4. 同步触发,注意动画闭包onSizeChange 在布局流程中同步调用,动画场景下建议用 setTimeout(..., 0) 延迟处理,避免状态被动画闭包捕获
  5. 触发次数永远 ≤ onAreaChange :仅位置移动(宽高不变)时只触发 onAreaChangeonSizeChange 不触发------这是两者的核心区别

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!

相关推荐
ITUnicorn21 天前
【HarmonyOS 6】进度组件实战:打造精美的数据可视化
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn25 天前
【HarmonyOS 6】数据可视化:实现热力图时间块展示
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS 6】HarmonyOS 自定义时间选择器实现
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】ArkTS 自定义组件封装实战:动画水杯组件
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】从零实现随机数生成器
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】从零实现自定义计时器:掌握TextTimer组件与计时控制
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
【HarmonyOS6】简易计数器开发
华为·harmonyos·arkts·鸿蒙·harmonyos6
ITUnicorn1 个月前
Flutter x HarmonyOS 6:依托小艺开放平台创建智能体并在应用中调用
flutter·harmonyos·鸿蒙·智能体·harmonyos6
ITUnicorn1 个月前
Flutter调用HarmonyOS6原生功能:实现智感握持
flutter·华为·harmonyos·harmonyos6·智感握持