5  第4章 高级UI开发

HarmonyOS应用开发者高级认证教程

6 第4章 高级UI开发

6.1 学习目标

本章将深入讲解HarmonyOS ArkUI框架的高级UI开发技术,涵盖自定义组件的深层机制、Canvas自定义绘制、高级动画系统、手势与交互、自定义布局以及声明式语法增强等核心知识。通过本章学习,读者将能够:

  • 深入理解自定义组件生命周期与性能优化策略
  • 掌握Canvas绑定模式与自定义绘制技术
  • 实现复杂动画效果,包括转场动画、弹簧物理动画与共享元素转场
  • 灵活运用手势系统实现复杂交互逻辑
  • 实现自定义布局接口,完成流式布局与瀑布流布局
  • 掌握LazyForEach等声明式语法的高级用法

6.2 4.1 自定义组件深入

6.2.1 4.1.1 自定义组件的生命周期

在HarmonyOS ArkUI中,自定义组件(@Component)拥有一套完整的生命周期回调机制。理解这些回调的触发时机和执行顺序,是编写高性能UI的基础。

自定义组件的生命周期主要包括以下阶段:

  1. aboutToAppear():组件即将创建,在build()方法之前执行。适合进行数据初始化、网络请求等操作。
  2. aboutToDisappear():组件即将销毁。适合进行资源释放、定时器清理、事件解绑等操作。
  3. onDidBuild():组件build()方法执行完毕后回调。
@Component
struct LifecycleDemo {
  @State message: string = '加载中...'
  private timerId: number = -1

  // 组件即将出现,在build()之前执行
  aboutToAppear(): void {
    console.info('[Lifecycle] aboutToAppear 被调用')
    // 适合进行数据初始化
    this.fetchData()
    // 启动定时器
    this.timerId = setInterval(() => {
      console.info('定时器执行中...')
    }, 1000)
  }

  // 组件即将销毁
  aboutToDisappear(): void {
    console.info('[Lifecycle] aboutToDisappear 被调用')
    // 必须清理定时器,防止内存泄漏
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
      this.timerId = -1
    }
  }

  // build()执行完毕后的回调
  onDidBuild?(): void {
    console.info('[Lifecycle] onDidBuild 被调用')
  }

  async fetchData(): Promise<void> {
    // 模拟异步数据获取
    this.message = '数据加载完成'
  }

  build() {
    Column() {
      Text(this.message)
        .fontSize(20)
    }
  }
}

生命周期执行顺序的关键细节:

  • aboutToAppear() 在组件首次创建时调用一次,组件因状态变化重新渲染时不会再次调用。
  • 如果父组件通过条件渲染(if)移除子组件后又重新创建,子组件的 aboutToAppear() 会再次执行。
  • aboutToDisappear() 在组件从组件树中永久移除时调用。

6.2.2 4.1.2 组件参数传递与验证

ArkUI提供了多种装饰器用于组件间参数传递,高级开发中需要深入理解每种装饰器的行为差异。

装饰器对比:

装饰器 数据流向 是否可修改 同步机制
@Prop 父→子 子组件可修改副本 单向同步
@Link 父↔︎子 子组件可修改引用 双向同步
@Provide/@Consume 祖先→后代 可修改 跨层级双向同步
@Observed/@ObjectLink 父→子 嵌套对象可观察 深层属性监听
// 使用 @Observed 标记嵌套对象类
@Observed
class UserProfile {
  name: string
  age: number
  address: AddressInfo

  constructor(name: string, age: number, address: AddressInfo) {
    this.name = name
    this.age = age
    this.address = address
  }
}

@Observed
class AddressInfo {
  city: string
  district: string

  constructor(city: string, district: string) {
    this.city = city
    this.district = district
  }
}

// 父组件
@Component
struct ParentComponent {
  @State profile: UserProfile = new UserProfile('张三', 28,
    new AddressInfo('深圳', '南山区'))

  build() {
    Column() {
      Text(`父组件: ${this.profile.name}`)
      // 使用 @ObjectLink 传递嵌套对象
      ChildComponent({ profile: this.profile })
      Button('修改城市')
        .onClick(() => {
          // 修改嵌套属性,子组件会自动更新
          this.profile.address.city = '北京'
        })
    }
  }
}

// 子组件
@Component
struct ChildComponent {
  // @ObjectLink 接收 @Observed 标记的对象
  @ObjectLink profile: UserProfile

  build() {
    Column() {
      Text(`子组件: ${this.profile.name}`)
      Text(`城市: ${this.profile.address.city}`)
        .fontSize(16)
        .fontColor(Color.Blue)
    }
  }
}

组件参数验证模式:

在高级开发中,建议在 aboutToAppear() 中对传入参数进行验证:

@Component
struct ValidatedInput {
  @Prop inputValue: string = ''
  @State isValid: boolean = true
  @State errorMessage: string = ''

  aboutToAppear(): void {
    this.validate(this.inputValue)
  }

  private validate(value: string): void {
    if (value.length < 3) {
      this.isValid = false
      this.errorMessage = '输入长度不能少于3个字符'
    } else if (value.length > 50) {
      this.isValid = false
      this.errorMessage = '输入长度不能超过50个字符'
    } else {
      this.isValid = true
      this.errorMessage = ''
    }
  }

  build() {
    Column() {
      Text(this.inputValue)
        .fontColor(this.isValid ? Color.Black : Color.Red)
      if (!this.isValid) {
        Text(this.errorMessage)
          .fontSize(12)
          .fontColor(Color.Red)
      }
    }
  }
}

6.2.3 4.1.3 组件复用与抽象

良好的组件抽象是构建大型应用的关键。以下是组件复用的核心策略:

策略一:泛型组件模式

// 定义通用列表项接口
interface ListItemData {
  id: string
  title: string
}

// 通用列表容器组件
@Component
struct GenericList<T extends ListItemData> {
  items: T[] = []
  @BuilderParam itemBuilder: (item: T) => void = this.defaultBuilder

  @Builder
  defaultBuilder(item: T) {
    Text(item.title)
  }

  build() {
    List() {
      ForEach(this.items, (item: T) => {
        ListItem() {
          this.itemBuilder(item)
        }
      }, (item: T) => item.id)
    }
  }
}

策略二:@BuilderParam@Builder 封装

@Component
struct CardContainer {
  @BuilderParam header: () => void = this.defaultHeader
  @BuilderParam content: () => void = this.defaultContent
  @BuilderParam footer: () => void = this.emptyBuilder
  shadow: boolean = true

  @Builder
  defaultHeader() {
    Text('默认标题').fontSize(18).fontWeight(FontWeight.Bold)
  }

  @Builder
  defaultContent() {
    Text('默认内容').fontSize(14)
  }

  @Builder
  emptyBuilder() {}

  build() {
    Column() {
      // 头部区域
      Row() {
        this.header()
      }
      .width('100%')
      .padding(12)

      Divider()

      // 内容区域
      Column() {
        this.content()
      }
      .width('100%')
      .padding(12)

      // 底部区域(可选)
      if (this.footer !== this.emptyBuilder) {
        Divider()
        Row() {
          this.footer()
        }
        .width('100%')
        .padding(12)
      }
    }
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow(this.shadow ? {
      radius: 8,
      color: 'rgba(0,0,0,0.15)',
      offsetX: 0,
      offsetY: 2
    } : { radius: 0, color: Color.Transparent, offsetX: 0, offsetY: 0 })
  }
}

// 使用示例
@Entry
@Component
struct CardUsageExample {
  build() {
    Column() {
      CardContainer({
        shadow: true,
        header: () => {
          this.customHeader()
        },
        content: () => {
          this.customContent()
        },
        footer: () => {
          this.customFooter()
        }
      })
    }
    .padding(16)
    .backgroundColor('#F1F3F5')
  }

  @Builder
  customHeader() {
    Row() {
      Image($r('app.media.app_icon'))
        .width(40)
        .height(40)
        .borderRadius(20)
      Column() {
        Text('用户卡片').fontSize(16).fontWeight(FontWeight.Bold)
        Text('在线').fontSize(12).fontColor(Color.Green)
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })
    }
  }

  @Builder
  customContent() {
    Text('这是卡片的自定义内容区域,可以放置任意UI元素。')
      .fontSize(14)
  }

  @Builder
  customFooter() {
    Row({ space: 12 }) {
      Button('点赞').type(ButtonType.Capsule).height(32)
      Button('评论').type(ButtonType.Capsule).height(32)
      Button('分享').type(ButtonType.Capsule).height(32)
    }
    .justifyContent(FlexAlign.End)
    .width('100%')
  }
}

6.2.4 4.1.4 自定义组件的性能优化

优化策略一:使用 @State 精确控制刷新范围

// 反模式:整个组件因单个状态变化而重建
@Component
struct BadPractice {
  @State title: string = ''
  @State count: number = 0
  @State list: string[] = []

  build() {
    Column() {
      Text(this.title)     // 只需要 title
      Text(`${this.count}`) // 只需要 count
      ForEach(this.list, (item: string) => {
        Text(item)          // 只需要 list
      })
    }
  }
}

// 优化方案:拆分组件,缩小刷新范围
@Component
struct TitleView {
  @Prop title: string = ''
  build() {
    Text(this.title)
  }
}

@Component
struct CounterView {
  @Prop count: number = 0
  build() {
    Text(`${this.count}`)
  }
}

@Component
struct ListView {
  @Prop list: string[] = []
  build() {
    ForEach(this.list, (item: string) => {
      Text(item)
    }, (item: string) => item)
  }
}

@Component
struct GoodPractice {
  @State title: string = ''
  @State count: number = 0
  @State list: string[] = []

  build() {
    Column() {
      TitleView({ title: this.title })       // 只有 title 变化时刷新
      CounterView({ count: this.count })      // 只有 count 变化时刷新
      ListView({ list: this.list })           // 只有 list 变化时刷新
    }
  }
}

优化策略二:使用 @Watch 避免不必要的计算

@Component
struct WatchOptimization {
  @State searchText: string = ''
  @State filteredList: string[] = []
  allItems: string[] = []

  // 使用 @Watch 精确监听变化
  @Watch('onSearchChange')
  @State debouncedSearch: string = ''

  onSearchChange(): void {
    // 只在搜索词真正变化时执行过滤
    this.filterItems(this.debouncedSearch)
  }

  private filterItems(keyword: string): void {
    if (keyword.length === 0) {
      this.filteredList = [...this.allItems]
    } else {
      this.filteredList = this.allItems.filter(
        (item: string) => item.includes(keyword)
      )
    }
  }

  build() {
    Column() {
      TextInput({ text: this.searchText, placeholder: '搜索...' })
        .onChange((value: string) => {
          this.searchText = value
          // 防抖处理
          this.debouncedSearch = value
        })

      List() {
        ForEach(this.filteredList, (item: string) => {
          ListItem() {
            Text(item)
          }
        }, (item: string) => item)
      }
    }
  }
}

6.3 4.2 自定义绘制

6.3.1 4.2.1 Canvas组件与绑定模式

ArkUI提供了两种Canvas使用模式:声明式Canvas绑定式Canvas。绑定模式通过 CanvasRenderingContext2D 提供了更灵活的绘制能力。

// 绑定模式Canvas示例
@Component
struct BoundCanvasDemo {
  // 创建离屏渲染上下文
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private context: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.settings)
  @State canvasWidth: number = 300
  @State canvasHeight: number = 400

  build() {
    Column() {
      Canvas(this.context)
        .width(this.canvasWidth)
        .height(this.canvasHeight)
        .backgroundColor('#F5F5F5')
        .onReady(() => {
          this.drawContent()
        })
    }
  }

  private drawContent(): void {
    const ctx = this.context

    // 清空画布
    ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

    // 绘制矩形
    ctx.fillStyle = '#4FC3F7'
    ctx.fillRect(20, 20, 120, 80)

    // 绘制描边矩形
    ctx.strokeStyle = '#E91E63'
    ctx.lineWidth = 3
    ctx.strokeRect(160, 20, 120, 80)

    // 绘制圆形
    ctx.beginPath()
    ctx.arc(80, 200, 50, 0, Math.PI * 2)
    ctx.fillStyle = '#66BB6A'
    ctx.fill()

    // 绘制文本
    ctx.font = '20px sans-serif'
    ctx.fillStyle = '#333333'
    ctx.fillText('Canvas绑定模式', 60, 300)
  }
}

6.3.2 4.2.2 图形绘制API

路径绘制与贝塞尔曲线:

@Component
struct PathDrawingDemo {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.settings)

  build() {
    Canvas(this.ctx)
      .width(360)
      .height(500)
      .onReady(() => {
        this.drawPaths()
      })
  }

  private drawPaths(): void {
    const ctx = this.ctx

    // 1. 绘制直线段路径 - 折线图
    ctx.beginPath()
    ctx.moveTo(20, 100)
    ctx.lineTo(80, 40)
    ctx.lineTo(140, 80)
    ctx.lineTo(200, 30)
    ctx.lineTo(260, 70)
    ctx.lineTo(320, 20)
    ctx.strokeStyle = '#2196F3'
    ctx.lineWidth = 2
    ctx.stroke()

    // 2. 绘制二次贝塞尔曲线
    ctx.beginPath()
    ctx.moveTo(20, 200)
    ctx.quadraticCurveTo(180, 120, 340, 200)
    ctx.strokeStyle = '#FF5722'
    ctx.lineWidth = 3
    ctx.stroke()

    // 3. 绘制三次贝塞尔曲线
    ctx.beginPath()
    ctx.moveTo(20, 300)
    ctx.bezierCurveTo(100, 220, 260, 380, 340, 300)
    ctx.strokeStyle = '#9C27B0'
    ctx.lineWidth = 3
    ctx.stroke()

    // 4. 绘制心形路径
    this.drawHeart(ctx, 180, 420, 60)
  }

  private drawHeart(ctx: CanvasRenderingContext2D,
      cx: number, cy: number, size: number): void {
    ctx.beginPath()
    ctx.moveTo(cx, cy + size / 4)
    // 左半部分
    ctx.bezierCurveTo(
      cx, cy - size / 4,
      cx - size / 2, cy - size / 4,
      cx - size / 2, cy + size / 8
    )
    ctx.bezierCurveTo(
      cx - size / 2, cy + size / 3,
      cx, cy + size / 2,
      cx, cy + size * 0.6
    )
    // 右半部分
    ctx.bezierCurveTo(
      cx, cy + size / 2,
      cx + size / 2, cy + size / 3,
      cx + size / 2, cy + size / 8
    )
    ctx.bezierCurveTo(
      cx + size / 2, cy - size / 4,
      cx, cy - size / 4,
      cx, cy + size / 4
    )
    ctx.fillStyle = '#E91E63'
    ctx.fill()
  }
}

变换与渐变:

@Component
struct TransformGradientDemo {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.settings)

  build() {
    Canvas(this.ctx)
      .width(360)
      .height(500)
      .onReady(() => {
        this.drawTransforms()
      })
  }

  private drawTransforms(): void {
    const ctx = this.ctx

    // 1. 线性渐变
    const linearGradient = ctx.createLinearGradient(20, 20, 320, 20)
    linearGradient.addColorStop(0, '#FF6B6B')
    linearGradient.addColorStop(0.5, '#4ECDC4')
    linearGradient.addColorStop(1, '#45B7D1')
    ctx.fillStyle = linearGradient
    ctx.fillRect(20, 20, 300, 60)

    // 2. 径向渐变
    const radialGradient = ctx.createRadialGradient(180, 200, 10, 180, 200, 80)
    radialGradient.addColorStop(0, '#FFD93D')
    radialGradient.addColorStop(0.6, '#FF6B6B')
    radialGradient.addColorStop(1, '#6C5CE7')
    ctx.beginPath()
    ctx.arc(180, 200, 80, 0, Math.PI * 2)
    ctx.fillStyle = radialGradient
    ctx.fill()

    // 3. 旋转变换
    ctx.save()
    ctx.translate(100, 380)
    ctx.rotate(Math.PI / 6) // 旋转30度
    ctx.fillStyle = '#00B894'
    ctx.fillRect(-40, -20, 80, 40)
    ctx.restore()

    // 4. 缩放变换
    ctx.save()
    ctx.translate(260, 380)
    ctx.scale(1.5, 0.8)
    ctx.fillStyle = '#6C5CE7'
    ctx.fillRect(-30, -25, 60, 50)
    ctx.restore()
  }
}

6.3.3 4.2.3 离屏渲染与缓存策略

对于复杂图形,可以使用离屏Canvas进行预渲染,避免每帧重复计算:

@Component
struct OffscreenCacheDemo {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.settings)
  // 离屏Canvas用于缓存
  private offscreenSettings: RenderingContextSettings =
    new RenderingContextSettings(true)
  private offscreenCtx: CanvasRenderingContext2D =
    new CanvasRenderingContext2D(this.offscreenSettings)
  @State isCacheReady: boolean = false
  @State rotationAngle: number = 0

  build() {
    Column() {
      Canvas(this.ctx)
        .width(360)
        .height(360)
        .onReady(() => {
          this.buildCache()
        })

      Slider({
        value: this.rotationAngle,
        min: 0,
        max: 360,
        step: 1
      })
        .onChange((value: number) => {
          this.rotationAngle = value
          this.renderFromCache()
        })
    }
  }

  // 构建离屏缓存
  private buildCache(): void {
    // 在离屏上下文绘制复杂图形
    this.offscreenCtx.fillStyle = '#FF6B6B'
    this.offscreenCtx.beginPath()
    this.offscreenCtx.arc(100, 100, 80, 0, Math.PI * 2)
    this.offscreenCtx.fill()

    // 添加复杂细节
    for (let i = 0; i < 12; i++) {
      const angle = (Math.PI * 2 / 12) * i
      const x = 100 + Math.cos(angle) * 60
      const y = 100 + Math.sin(angle) * 60
      this.offscreenCtx.beginPath()
      this.offscreenCtx.arc(x, y, 10, 0, Math.PI * 2)
      this.offscreenCtx.fillStyle = '#4ECDC4'
      this.offscreenCtx.fill()
    }

    this.isCacheReady = true
    this.renderFromCache()
  }

  // 从缓存渲染到主Canvas
  private renderFromCache(): void {
    this.ctx.clearRect(0, 0, 360, 360)
    this.ctx.save()
    this.ctx.translate(180, 180)
    this.ctx.rotate(this.rotationAngle * Math.PI / 180)
    this.ctx.translate(-100, -100)
    // 直接绘制缓存的图像数据
    this.ctx.drawImage(this.offscreenCtx, 0, 0)
    this.ctx.restore()
  }
}

6.4 4.3 高级动画系统

6.4.1 4.3.1 属性动画与显式动画

ArkUI提供了两种基本动画方式:属性动画(隐式动画)和显式动画

@Component
struct AnimationDemo {
  @State boxWidth: number = 100
  @State boxColor: string = '#4FC3F7'
  @State boxRadius: number = 8
  @State opacity: number = 1.0

  build() {
    Column({ space: 20 }) {
      // 属性动画:状态变化自动触发过渡
      Row() {
        Text('属性动画')
          .fontSize(14)
          .fontColor(Color.White)
      }
      .width(this.boxWidth)
      .height(60)
      .backgroundColor(this.boxColor)
      .borderRadius(this.boxRadius)
      .opacity(this.opacity)
      // 使用 .animation() 修饰属性变化
      .animation({
        duration: 500,
        curve: Curve.EaseInOut,
        delay: 0,
        fill: 'forwards',
        iterations: 1,
        playMode: PlayMode.Normal
      })

      // 显式动画:使用 animateTo
      Button('显式动画 - 展开')
        .onClick(() => {
          animateTo({
            duration: 800,
            curve: Curve.FastOutSlowIn,
            onFinish: () => {
              console.info('动画执行完毕')
            }
          }, () => {
            this.boxWidth = 300
            this.boxColor = '#66BB6A'
            this.boxRadius = 30
          })
        })

      Button('显式动画 - 收缩')
        .onClick(() => {
          animateTo({
            duration: 600,
            curve: Curve.FastOutSlowIn
          }, () => {
            this.boxWidth = 100
            this.boxColor = '#4FC3F7'
            this.boxRadius = 8
          })
        })

      Button('透明度动画')
        .onClick(() => {
          animateTo({
            duration: 1000,
            curve: Curve.EaseInOut,
            iterations: 2,
            playMode: PlayMode.AlternateReverse
          }, () => {
            this.opacity = this.opacity === 1.0 ? 0.1 : 1.0
          })
        })
    }
    .width('100%')
    .padding(20)
    .alignItems(HorizontalAlign.Center)
  }
}

6.4.2 4.3.2 转场动画

页面转场与组件转场:

// 自定义页面转场
@Entry
@Component
struct PageTransitionDemo {
  @State isActive: boolean = false

  build() {
    Column({ space: 20 }) {
      Text('页面转场演示')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      // 组件转场 - 使用 .transition()
      if (this.isActive) {
        Column() {
          Text('转场组件')
            .fontSize(20)
            .fontColor(Color.White)
        }
        .width(200)
        .height(200)
        .backgroundColor('#FF6B6B')
        .borderRadius(16)
        .justifyContent(FlexAlign.Center)
        // 自定义插入转场
        .transition(TransitionEffect.OPACITY.combine(
          TransitionEffect.translate({ x: 200 }).animation({
            duration: 600,
            curve: Curve.EaseOut
          })
        ).combine(
          TransitionEffect.scale({ x: 0.5, y: 0.5 }).animation({
            duration: 600,
            curve: Curve.EaseOut
          })
        ))
      }

      Button(this.isActive ? '移除组件' : '添加组件')
        .onClick(() => {
          this.isActive = !this.isActive
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

页面级别的转场配置:

@Entry
@Component
struct PageLevelTransition {
  @State navigateNext: boolean = false

  build() {
    NavDestination() {
      Column() {
        Text('详情页')
          .fontSize(24)
      }
    }
    // 页面进入转场
    .onPageShow(() => {
      console.info('页面显示')
    })
    // 页面退出转场
    .onPageHide(() => {
      console.info('页面隐藏')
    })
    .transition(TransitionEffect.OPACITY.combine(
      TransitionEffect.translate({ y: 100 })
    ))
  }
}

6.4.3 4.3.3 弹簧物理动画

弹簧动画通过物理模型实现更自然的动画效果:

@Component
struct SpringAnimationDemo {
  @State ballX: number = 50
  @State ballScale: number = 1.0

  build() {
    Column({ space: 30 }) {
      // 弹簧动画小球
      Circle()
        .width(60)
        .height(60)
        .fill('#FF6B6B')
        .translate({ x: this.ballX })
        .scale({ x: this.ballScale, y: this.ballScale })

      // 弹簧参数说明:
      // response: 弹簧响应时间(毫秒),值越大动画越慢
      // dampingFraction: 阻尼系数(0-1),值越小弹跳越多
      // overlap: 是否允许过冲
      Button('弹簧动画 - 弹跳')
        .onClick(() => {
          animateTo({
            duration: 1000,
            curve: {
              curve: CurveStatus.Spring,
              // 自定义弹簧参数
            } as SpringCurve,
            iterations: 1
          }, () => {
            this.ballX = this.ballX === 50 ? 250 : 50
          })
        })

      Button('缩放弹簧')
        .onClick(() => {
          animateTo({
            duration: 800,
            curve: Curve.Friction
          }, () => {
            this.ballScale = this.ballScale === 1.0 ? 1.8 : 1.0
          })
        })

      // 使用 geometryTransition 实现几何变换
      Row() {
        Circle()
          .width(this.ballScale > 1.2 ? 100 : 60)
          .height(this.ballScale > 1.2 ? 100 : 60)
          .fill('#4ECDC4')
          .geometryTransition('sharedCircle')
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .padding(20)
  }
}

6.4.4 4.3.4 帧动画实现

@Component
struct FrameAnimationDemo {
  @State currentFrame: number = 0
  @State isPlaying: boolean = false
  private frameCount: number = 24
  private timerId: number = -1
  private fps: number = 24

  // 帧资源列表
  private frameResources: Resource[] = []

  aboutToAppear(): void {
    // 加载帧资源
    for (let i = 1; i <= this.frameCount; i++) {
      this.frameResources.push($r(`app.media.frame_${i.toString().padStart(3, '0')}`))
    }
  }

  aboutToDisappear(): void {
    this.stopAnimation()
  }

  private startAnimation(): void {
    if (this.isPlaying) return
    this.isPlaying = true
    const interval = 1000 / this.fps
    this.timerId = setInterval(() => {
      this.currentFrame = (this.currentFrame + 1) % this.frameCount
    }, interval)
  }

  private stopAnimation(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
      this.timerId = -1
    }
    this.isPlaying = false
  }

  build() {
    Column({ space: 20 }) {
      // 显示当前帧
      Image(this.frameResources[this.currentFrame])
        .width(200)
        .height(200)
        .objectFit(ImageFit.Contain)

      // 帧进度指示
      Text(`帧: ${this.currentFrame + 1} / ${this.frameCount}`)
        .fontSize(14)

      Row({ space: 20 }) {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) {
              this.stopAnimation()
            } else {
              this.startAnimation()
            }
          })

        Button('重置')
          .onClick(() => {
            this.stopAnimation()
            this.currentFrame = 0
          })
      }
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
  }
}

6.5 4.4 手势与交互

6.5.1 4.4.1 手势系统详解

ArkUI提供了丰富的内置手势识别器:

手势类型 说明 触发条件
TapGesture 点击手势 按下并抬起
LongPressGesture 长按手势 按住超过500ms
PanGesture 拖拽手势 按住并移动
PinchGesture 捏合手势 双指缩放
RotationGesture 旋转手势 双指旋转
SwipeGesture 滑动手势 快速滑动
@Component
struct GestureSystemDemo {
  @State offsetX: number = 0
  @State offsetY: number = 0
  @State scale: number = 1.0
  @State rotation: number = 0
  @State gestureText: string = '等待手势...'
  @State lastScale: number = 1.0
  @State lastRotation: number = 0

  build() {
    Column() {
      Text(this.gestureText)
        .fontSize(14)
        .margin({ bottom: 20 })

      // 多手势组合示例
      Rect()
        .width(200)
        .height(200)
        .fill('#4FC3F7')
        .borderRadius(16)
        .translate({ x: this.offsetX, y: this.offsetY })
        .scale({ x: this.scale, y: this.scale })
        .rotate({ angle: this.rotation })
        // 拖拽手势
        .gesture(
          PanGesture({ fingers: 1, distance: 5 })
            .onActionStart(() => {
              this.gestureText = '拖拽开始'
            })
            .onActionUpdate((event: GestureEvent) => {
              this.offsetX = event.offsetX
              this.offsetY = event.offsetY
              this.gestureText =
                `拖拽中: (${event.offsetX.toFixed(0)}, ${event.offsetY.toFixed(0)})`
            })
            .onActionEnd(() => {
              this.gestureText = '拖拽结束'
            })
        )
        // 捏合手势(通过 parallelGesture 并行识别)
        .parallelGesture(
          PinchGesture({ fingers: 2 })
            .onActionStart(() => {
              this.lastScale = this.scale
              this.gestureText = '捏合开始'
            })
            .onActionUpdate((event: GestureEvent) => {
              this.scale = this.lastScale * event.scale
              this.gestureText = `缩放: ${this.scale.toFixed(2)}`
            })
        )
        // 旋转手势
        .parallelGesture(
          RotationGesture({ fingers: 2 })
            .onActionStart(() => {
              this.lastRotation = this.rotation
              this.gestureText = '旋转开始'
            })
            .onActionUpdate((event: GestureEvent) => {
              this.rotation = this.lastRotation + event.angle
              this.gestureText = `旋转: ${this.rotation.toFixed(1)}°`
            })
        )

      // 重置按钮
      Button('重置变换')
        .margin({ top: 30 })
        .onClick(() => {
          animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
            this.offsetX = 0
            this.offsetY = 0
            this.scale = 1.0
            this.rotation = 0
          })
          this.gestureText = '已重置'
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

6.5.2 4.4.2 手势组合与竞争

ArkUI支持三种手势组合方式:

  • GestureGroup(GestureMode.Sequence):顺序识别,按顺序完成所有手势
  • GestureGroup(GestureMode.Parallel):并行识别,同时识别多个手势
  • GestureGroup(GestureMode.Exclusive):互斥识别,只识别第一个匹配的手势
@Component
struct GestureCombinationDemo {
  @State statusText: string = '等待操作...'
  @State tapCount: number = 0

  build() {
    Column({ space: 20 }) {
      Text(this.statusText)
        .fontSize(16)

      // 示例1:互斥手势 - 单击 vs 双击
      Column() {
        Text('互斥手势:单击/双击')
          .fontSize(14)
          .fontColor(Color.Gray)

        Text('点击此区域')
          .width(300)
          .height(80)
          .textAlign(TextAlign.Center)
          .backgroundColor('#E3F2FD')
          .borderRadius(12)
          .gestureGroup(GestureMode.Exclusive,
            // 双击优先
            TapGesture({ count: 2 })
              .onAction(() => {
                this.statusText = '检测到双击!'
              }),
            // 单击
            TapGesture({ count: 1 })
              .onAction(() => {
                this.tapCount++
                this.statusText = `单击次数: ${this.tapCount}`
              })
          )
      }

      // 示例2:顺序手势 - 长按后拖拽
      Column() {
        Text('顺序手势:长按 → 拖拽')
          .fontSize(14)
          .fontColor(Color.Gray)

        Text('长按后拖拽')
          .width(300)
          .height(80)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FFF3E0')
          .borderRadius(12)
          .gesture(
            GestureGroup(GestureMode.Sequence,
              LongPressGesture({ duration: 500 })
                .onAction(() => {
                  this.statusText = '长按已识别,请拖拽...'
                }),
              PanGesture({ distance: 10 })
                .onActionUpdate((event: GestureEvent) => {
                  this.statusText =
                    `拖拽中: (${event.offsetX.toFixed(0)}, ${event.offsetY.toFixed(0)})`
                })
            )
          )
      }

      // 示例3:并行手势
      Column() {
        Text('并行手势:拖拽 + 缩放')
          .fontSize(14)
          .fontColor(Color.Gray)

        Text('单指拖拽 / 双指缩放')
          .width(300)
          .height(120)
          .textAlign(TextAlign.Center)
          .backgroundColor('#E8F5E9')
          .borderRadius(12)
          .gesture(
            GestureGroup(GestureMode.Parallel,
              PanGesture()
                .onActionUpdate(() => {
                  this.statusText = '拖拽识别中'
                }),
              PinchGesture()
                .onActionUpdate(() => {
                  this.statusText = '缩放识别中'
                })
            )
          )
      }
    }
    .width('100%')
    .padding(20)
  }
}

6.5.3 4.4.3 拖拽与滑动交互

@Component
struct DragDropDemo {
  @State items: DragItem[] = [
    new DragItem('1', '项目 A', '#FF6B6B'),
    new DragItem('2', '项目 B', '#4ECDC4'),
    new DragItem('3', '项目 C', '#45B7D1'),
    new DragItem('4', '项目 D', '#96CEB4'),
  ]
  @State dragIndex: number = -1
  @State dropIndex: number = -1

  build() {
    Column() {
      Text('拖拽排序列表')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 16 })

      ForEach(this.items, (item: DragItem, index: number) => {
        Row() {
          Text(item.label)
            .fontSize(16)
            .fontColor(Color.White)
            .layoutWeight(1)
        }
        .width('100%')
        .height(60)
        .padding({ left: 16, right: 16 })
        .backgroundColor(item.color)
        .borderRadius(8)
        .margin({ bottom: 8 })
        .opacity(this.dragIndex === index ? 0.5 : 1.0)
        // 拖拽能力
        .dragable(true)
        .onDragStart(() => {
          this.dragIndex = index
          // 返回拖拽预览
          return this.getDragPreview(item)
        })
        .onDrop((event: DragEvent, extraParams: string) => {
          this.dropIndex = index
          this.reorderItems(this.dragIndex, this.dropIndex)
          this.dragIndex = -1
          this.dropIndex = -1
        })
      }, (item: DragItem) => item.id)
    }
    .width('100%')
    .padding(16)
  }

  @Builder
  getDragPreview(item: DragItem): void {
    // 此处返回自定义拖拽预览(实际返回像素图)
  }

  private reorderItems(from: number, to: number): void {
    if (from === to || from < 0 || to < 0) return
    const temp = this.items[from]
    const newItems = [...this.items]
    newItems.splice(from, 1)
    newItems.splice(to, 0, temp)
    this.items = newItems
  }
}

class DragItem {
  id: string
  label: string
  color: string

  constructor(id: string, label: string, color: string) {
    this.id = id
    this.label = label
    this.color = color
  }
}

6.6 4.5 自定义布局

6.6.1 4.5.1 Layout接口实现

ArkUI提供了自定义布局接口,允许开发者实现完全自定义的布局逻辑:

// 自定义流式布局
@Builder
function FlowLayoutBuilder() {
  FlowLayout({ space: 10 }) {
    ForEach(['HarmonyOS', 'ArkTS', '分布式', '软总线',
      '超级终端', '跨设备', '原子化服务', '元服务',
      'Stage模型', '声明式UI'], (tag: string) => {
      Text(tag)
        .fontSize(14)
        .fontColor(Color.White)
        .backgroundColor('#4FC3F7')
        .borderRadius(16)
        .padding({ left: 12, right: 12, top: 6, bottom: 6 })
    }, (tag: string) => tag)
  }
}

// 流式布局组件实现
@Component
struct FlowLayout {
  @BuilderParam content: () => void = this.defaultBuilder
  space: number = 10

  @Builder
  defaultBuilder() {}

  build() {
    Column() {
      this.content()
    }
    // 使用 measure 和 layout 回调实现自定义布局
    .onAreaChange((oldArea: Area, newArea: Area) => {
      // 容器尺寸变化时重新布局
    })
  }
}

使用自定义Layout接口的流式布局:

// 通过 Layout 接口实现真正的自定义布局
@Component
struct CustomFlowLayout {
  space: number = 8
  @BuilderParam content: () => void = this.defaultContent

  @Builder
  defaultContent() {}

  build() {
    Layout(this) {
      this.content()
    }
  }

  // 实现 Layout 接口的 measure 方法
  measure(childIds: number[], constraint: ConstraintSize): MeasureResult {
    const containerWidth = constraint.maxWidth ?? 360
    let currentX = 0
    let currentY = 0
    let lineHeight = 0
    let totalHeight = 0

    for (const childId of childIds) {
      const childSize = this.getChildMeasureSize(childId)
      const childWidth = childSize.width
      const childHeight = childSize.height

      // 如果当前行放不下,换行
      if (currentX + childWidth > containerWidth && currentX > 0) {
        currentX = 0
        currentY += lineHeight + this.space
        lineHeight = 0
      }

      // 记录子组件位置
      this.setChildPosition(childId, currentX, currentY)
      currentX += childWidth + this.space
      lineHeight = Math.max(lineHeight, childHeight)
      totalHeight = currentY + lineHeight
    }

    return {
      width: containerWidth,
      height: totalHeight
    }
  }

  // 实现 Layout 接口的 place 方法
  place(childIds: number[]): void {
    for (const childId of childIds) {
      const pos = this.getChildPosition(childId)
      this.placeChild(childId, pos.x, pos.y)
    }
  }

  private getChildMeasureSize(childId: number):
    { width: number, height: number } {
    // 获取子组件的测量尺寸
    return { width: 100, height: 40 }
  }

  private setChildPosition(childId: number, x: number, y: number): void {
    // 存储子组件位置
  }

  private getChildPosition(childId: number): { x: number, y: number } {
    return { x: 0, y: 0 }
  }

  private placeChild(childId: number, x: number, y: number): void {
    // 放置子组件到指定位置
  }
}

6.6.2 4.5.2 瀑布流布局实现

// 瀑布流数据模型
@Observed
class WaterfallItem {
  id: number
  title: string
  height: number  // 内容高度
  color: string
  imageUri: string

  constructor(id: number, title: string, height: number,
      color: string, imageUri: string = '') {
    this.id = id
    this.title = title
    this.height = height
    this.color = color
    this.imageUri = imageUri
  }
}

// 瀑布流数据源
class WaterfallDataSource implements IDataSource {
  private items: WaterfallItem[] = []
  private listeners: DataChangeListener[] = []

  public totalCount(): number {
    return this.items.length
  }

  public getData(index: number): WaterfallItem {
    return this.items[index]
  }

  public registerDataChangeListener(
      listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  public unregisterDataChangeListener(
      listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  public addData(index: number, data: WaterfallItem): void {
    this.items.splice(index, 0, data)
    this.notifyDataAdd(index, 1)
  }

  public loadData(newItems: WaterfallItem[]): void {
    this.items = [...this.items, ...newItems]
    this.notifyDataAdd(this.items.length - newItems.length, newItems.length)
  }

  private notifyDataAdd(index: number, count: number): void {
    for (const listener of this.listeners) {
      listener.onDataAdd(index, count)
    }
  }
}

// 瀑布流组件
@Component
struct WaterfallLayout {
  @State dataSource: WaterfallDataSource = new WaterfallDataSource()
  columnCount: number = 2
  columnGap: number = 8
  columnHeights: number[] = [0, 0]

  aboutToAppear(): void {
    // 加载模拟数据
    const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
      '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F']
    const items: WaterfallItem[] = []
    for (let i = 0; i < 30; i++) {
      const height = 120 + Math.floor(Math.random() * 180)
      const color = colors[i % colors.length]
      items.push(new WaterfallItem(i, `内容 ${i + 1}`, height, color))
    }
    this.dataSource.loadData(items)
  }

  build() {
    WaterFlow() {
      LazyForEach(this.dataSource, (item: WaterfallItem) => {
        FlowItem() {
          Column() {
            // 模拟不同高度的内容
            Column()
              .width('100%')
              .height(item.height)
              .backgroundColor(item.color)
              .borderRadius(8)

            Text(item.title)
              .fontSize(14)
              .margin({ top: 6 })
              .padding({ left: 4, right: 4 })
          }
          .padding(4)
        }
      }, (item: WaterfallItem) => `waterfall_${item.id}`)
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(this.columnGap)
    .rowsGap(this.columnGap)
    .width('100%')
    .height('100%')
  }
}

6.7 4.6 ArkUI声明式语法增强

6.7.1 4.6.1 条件渲染与循环渲染优化

条件渲染最佳实践:

@Component
struct ConditionalRenderOptimization {
  @State userType: 'guest' | 'member' | 'admin' = 'guest'
  @State showDetails: boolean = false

  build() {
    Column() {
      // 优化1:使用 if-else 而非三元表达式控制复杂UI
      if (this.userType === 'admin') {
        AdminPanel()
          .transition(TransitionEffect.OPACITY)
      } else if (this.userType === 'member') {
        MemberPanel()
          .transition(TransitionEffect.OPACITY)
      } else {
        GuestPanel()
          .transition(TransitionEffect.OPACITY)
      }

      // 优化2:避免在循环中使用条件渲染
      // 反模式
      // ForEach(this.list, (item) => {
      //   if (item.isVisible) {
      //     ListItem() { ... }
      //   }
      // })

      // 正确做法:先过滤再渲染
      // ForEach(this.getVisibleItems(), (item) => {
      //   ListItem() { ... }
      // })
    }
  }

  private getVisibleItems(): DataItem[] {
    return [] // 返回过滤后的数据
  }
}

@Component struct AdminPanel {
  build() { Text('管理员面板').fontSize(18) }
}
@Component struct MemberPanel {
  build() { Text('会员面板').fontSize(18) }
}
@Component struct GuestPanel {
  build() { Text('访客面板').fontSize(18) }
}

6.7.2 4.6.2 LazyForEach深入

LazyForEach 是处理大数据列表的关键组件,它按需创建子组件,避免一次性渲染所有数据。

// 实现完整的 IDataSource 接口
class BasicDataSource implements IDataSource {
  private data: string[] = []
  private listeners: DataChangeListener[] = []

  totalCount(): number {
    return this.data.length
  }

  getData(index: number): string {
    return this.data[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }

  // 数据变更通知方法
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReload()
    })
  }

  notifyDataAdd(index: number, count: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index, count)
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index)
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index)
    })
  }

  // 设置数据
  setData(newData: string[]): void {
    this.data = newData
    this.notifyDataReload()
  }

  appendData(newItems: string[]): void {
    const startIndex = this.data.length
    this.data = [...this.data, ...newItems]
    this.notifyDataAdd(startIndex, newItems.length)
  }
}

@Component
struct LazyForEachDemo {
  private dataSource: BasicDataSource = new BasicDataSource()
  @State isLoadingMore: boolean = false
  @State totalCount: number = 0

  aboutToAppear(): void {
    // 初始加载
    const initialData: string[] = []
    for (let i = 0; i < 50; i++) {
      initialData.push(`列表项 ${i + 1}`)
    }
    this.dataSource.setData(initialData)
    this.totalCount = initialData.length
  }

  build() {
    Column() {
      Text(`总数: ${this.totalCount}`)
        .fontSize(16)
        .margin({ bottom: 8 })

      List() {
        LazyForEach(this.dataSource, (item: string, index: number) => {
          ListItem() {
            Row() {
              Text(`${index + 1}. ${item}`)
                .fontSize(16)
                .layoutWeight(1)
            }
            .width('100%')
            .height(60)
            .padding({ left: 16, right: 16 })
            .borderRadius(8)
            .margin({ bottom: 4 })
            .backgroundColor(index % 2 === 0 ? '#F5F5F5' : Color.White)
          }
        }, (item: string, index: number) => `item_${index}_${item}`)
      }
      .width('100%')
      .layoutWeight(1)
      // 滚动到底部时加载更多
      .onReachEnd(() => {
        this.loadMore()
      })

      if (this.isLoadingMore) {
        LoadingProgress()
          .width(40)
          .height(40)
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  private async loadMore(): Promise<void> {
    if (this.isLoadingMore) return
    this.isLoadingMore = true

    // 模拟网络请求
    const newItems: string[] = []
    const start = this.totalCount
    for (let i = 0; i < 20; i++) {
      newItems.push(`列表项 ${start + i + 1}`)
    }

    this.dataSource.appendData(newItems)
    this.totalCount += newItems.length
    this.isLoadingMore = false
  }
}

6.7.3 4.6.3 组件缓存与复用策略

@Component
struct CachedListItem {
  @Prop itemData: ListItemModel = new ListItemModel('', '', '')
  @State isImageLoaded: boolean = false

  build() {
    Row({ space: 12 }) {
      // 图片加载与缓存
      Image(this.itemData.imageUrl)
        .width(60)
        .height(60)
        .borderRadius(8)
        .objectFit(ImageFit.Cover)
        .onComplete(() => {
          this.isImageLoaded = true
        })
        .alt($r('app.media.placeholder')) // 占位图

      Column({ space: 4 }) {
        Text(this.itemData.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.itemData.subtitle)
          .fontSize(13)
          .fontColor('#666666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
  }
}

class ListItemModel {
  id: string
  title: string
  subtitle: string
  imageUrl: string

  constructor(id: string, title: string, subtitle: string,
      imageUrl: string = '') {
    this.id = id
    this.title = title
    this.subtitle = subtitle
    this.imageUrl = imageUrl
  }
}

组件缓存策略配置:

// 使用 componentCache 控制组件缓存
@Component
struct CacheStrategyDemo {
  private dataSource: CachedDataSource = new CachedDataSource()

  build() {
    List() {
      LazyForEach(this.dataSource, (item: ListItemModel) => {
        ListItem() {
          CachedListItem({ itemData: item })
        }
        .cachedCount(5) // 缓存屏幕外5个组件
      }, (item: ListItemModel) => item.id)
    }
    .cachedCount(5) // List级别的缓存数量
    .width('100%')
    .height('100%')
    .divider({
      strokeWidth: 0.5,
      color: '#EEEEEE',
      startMargin: 84,
      endMargin: 12
    })
  }
}

6.8 本章小结

本章深入讲解了HarmonyOS ArkUI框架的高级UI开发技术:

  1. 自定义组件:掌握了组件生命周期的三个阶段(aboutToAppearaboutToDisappearonDidBuild),理解了不同装饰器的数据流向差异,学会了通过组件拆分和 @Watch 进行性能优化。

  2. 自定义绘制:掌握了Canvas绑定模式的使用方法,能够使用路径、贝塞尔曲线、变换和渐变绘制复杂图形,理解了离屏渲染与缓存策略对性能的提升。

  3. 高级动画系统:掌握了属性动画与显式动画的区别,能够实现页面转场、组件转场、弹簧物理动画和帧动画。

  4. 手势与交互:理解了ArkUI手势系统的完整架构,掌握了手势组合(顺序、并行、互斥)的使用方法和拖拽交互的实现。

  5. 自定义布局:学会了通过Layout接口实现自定义布局,掌握了流式布局和瀑布流布局的实现方案。

  6. 声明式语法增强:深入理解了LazyForEach的工作机制,掌握了IDataSource接口的完整实现和组件缓存策略。


6.9 练习题

6.9.1 一、单选题

1. 在ArkUI自定义组件中,以下哪个生命周期回调在 build() 方法之前执行?

A. onDidBuild() B. aboutToDisappear() C. aboutToAppear() D. onPageShow()

答案:C

解析: aboutToAppear() 在组件即将创建时调用,在 build() 方法执行之前运行。onDidBuild()build() 之后调用。aboutToDisappear() 在组件销毁时调用。onPageShow() 是页面级生命周期,不属于自定义组件生命周期。


2. 以下哪种装饰器支持嵌套对象的深层属性监听?

A. @Prop B. @Link C. @Observed + @ObjectLink D. @Provide + @Consume

答案:C

解析: @Observed 标记类使其属性可被观察,@ObjectLink 在子组件中接收被 @Observed 标记的对象,两者配合使用可以实现嵌套对象属性的深层监听。当嵌套对象的属性发生变化时,使用 @ObjectLink 的子组件会自动刷新。


3. 在Canvas绑定模式中,创建 CanvasRenderingContext2D 时需要传入什么参数?

A. Canvas组件的引用 B. RenderingContextSettings 对象 C. 画布宽高尺寸 D. 渲染模式枚举值

答案:B

解析: 创建 CanvasRenderingContext2D 时需要传入 RenderingContextSettings 对象,该对象用于配置渲染上下文设置,如是否开启抗锯齿等。


4. 以下哪种手势组合模式表示”互斥识别”?

A. GestureMode.Sequence B. GestureMode.Parallel C. GestureMode.Exclusive D. GestureMode.Alternative

答案:C

解析: GestureMode.Exclusive 表示互斥识别模式,只会识别第一个匹配成功的手势。Sequence 是顺序识别,Parallel 是并行识别。


6.9.2 二、多选题

5. 以下哪些是ArkUI提供的内置手势识别器?

A. TapGesture B. LongPressGesture C. DoubleTapGesture D. PinchGesture E. RotationGesture

答案:A、B、D、E

解析: ArkUI内置手势包括 TapGesture(点击,可通过count参数设置双击)、LongPressGesture(长按)、PanGesture(拖拽)、PinchGesture(捏合)、RotationGesture(旋转)、SwipeGesture(滑动)。没有单独的 DoubleTapGesture,双击通过 TapGesture({ count: 2 }) 实现。


6. 关于 LazyForEach 的描述,以下哪些是正确的?

A. 按需创建子组件,避免一次性渲染所有数据 B. 需要配合实现 IDataSource 接口的数据源使用 C. 数据变更时必须调用 notifyDataReload() 通知刷新 D. 可以通过 keyGenerator 参数指定唯一键值生成规则 E. 只支持 List 组件,不支持其他容器

答案:A、B、D

解析: LazyForEach 确实按需创建子组件(A正确),需要 IDataSource 数据源(B正确),支持自定义键值生成器(D正确)。数据变更时可以根据情况调用不同的通知方法(增/删/改/重载),不一定必须调用 notifyDataReload()(C错误)。LazyForEach 可用于 ListGridWaterFlow 等多种容器(E错误)。


6.9.3 三、判断题

7. 在ArkUI中,@Prop 装饰器传递的是数据的引用,子组件对数据的修改会影响父组件。

答案:错误

解析: @Prop 传递的是数据的副本(深拷贝),子组件对 @Prop 修饰的数据进行修改不会影响父组件的原始数据。如果需要双向同步,应使用 @Link 装饰器。


8. 使用 animateTo 进行显式动画时,状态变量的修改必须放在 animateTo 的回调函数中。

答案:正确

解析: animateTo 的第二个参数是一个无参回调函数,所有需要动画效果的状态变量修改都必须放在这个回调中。框架会检测回调中状态变量的变化,并自动应用动画效果。