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 不触发------这是两者的核心区别

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

相关推荐
全栈若城1 天前
HarmonyOS6 半年磨一剑 - RcInput 组件清空、密码切换与图标交互机制
架构·交互·harmonyos6·三方库开发实战·rchoui·三方库开发
全栈若城6 天前
HarmonyOS 6 实战:Component3D 与 SURFACE 渲染模式深度解析
3d·架构·harmonyos6
全栈若城6 天前
HarmonyOS 6 实战:使用 ArkGraphics3D 加载 GLB 模型与 Scene 初始化全流程
3d·华为·架构·harmonyos·harmonyos6
是稻香啊14 天前
HarmonyOS6 ArkTS Popup 气泡组件指南
harmonyos6
是稻香啊14 天前
HarmonyOS6 触摸目标 touch-target 属性使用指南
harmonyos6
是稻香啊15 天前
HarmonyOS6 foregroundBlurStyle 通用属性使用指南
harmonyos6
是稻香啊15 天前
HarmonyOS6 clickEffect 通用属性使用指南
harmonyos6
是稻香啊15 天前
HarmonyOS6 filter 通用属性使用指南
harmonyos6
是稻香啊21 天前
HarmonyOS6 ArkUI 无障碍悬停事件(onAccessibilityHover)全面解析与实战演示
华为·harmonyos·harmonyos6
是稻香啊21 天前
HarmonyOS6 背景设置:background 基础属性全解析
harmonyos6