大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

一个组件淡入、淡出,看起来可能只是改一下 opacity。但如果这个组件本身还需要频繁插入和删除,继续用 animateTo 控制透明度,再在动画结束时修改显示状态,事情就没有表面上这么简单了。

这个问题很适合用来区分 ArkUI 里的两类动画:属性发生变化,以及组件本身出现/消失。

华为官方目前甚至提供了一条专门的 Code Linter 性能规则:@performance/hp-arkui-use-transition-to-replace-animateto,规则说明就是“建议组件转场动画使用 transition”,并将其列为动效丢帧场景下建议优先修改的问题。

这次就围绕一个最小场景,把 animateTo 和 transition 的边界拆开来看。

一、先做一个频繁显示/隐藏的组件

假设页面里有一段提示信息,需要反复显示和隐藏:

点击按钮
   ↓
显示提示
   ↓
淡入 / 淡出
   ↓
组件移除

如果从“透明度发生变化”这个角度理解,很容易写成下面这种结构:

@Entry
@Component
struct AnimateToDemo {
  @State show: boolean = true;
  @State contentOpacity: number = 1;

  build() {
    Column({ space: 20 }) {
      Row() {
        if (this.show) {
          Text('Network connected')
            .fontSize(20)
            .opacity(this.contentOpacity)
        }
      }
      .width('100%')
      .height(100)
      .justifyContent(FlexAlign.Center)

      Button('Toggle')
        .onClick(() => {
          this.show = true;

          this.getUIContext().animateTo({
            duration: 300,
            onFinish: () => {
              if (this.contentOpacity === 0) {
                this.show = false;
              }
            }
          }, () => {
            this.contentOpacity =
              this.contentOpacity === 1 ? 0 : 1;
          });
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这段代码解决的是:先保证组件存在,再对 opacity 做属性动画;淡出完成之后,再通过 show = false 把组件移出组件树。

需要说明的是,上面的代码用于展示这种实现结构,并没有声称已经在具体设备上编译、运行或测得某个帧率。

有意思的是,华为 Code Linter 官方规则给出的反例与这个结构基本一致:使用一个透明度状态和一个显示状态,通过 animateTo 修改透明度,并在 onFinish 中决定是否删除组件。官方建议把这种“组件出现/消失”的动画改成 transition。

【建议插图1:DevEco Studio 中 AnimateToDemo 的页面结构,以及连续点击 Toggle 时的录屏画面】

二、问题不是 animateTo 慢,而是动画类型选错了

这里比较容易产生一个误解:既然官方建议改成 transition,是不是说明 animateTo 性能不好?

不是。

animateTo 本身就是 ArkUI 的系统属性动画能力。官方《优化动画性能》文档明确把 animateTo 和 animation 作为属性动画接口,并建议在系统动画接口能够满足需求时优先使用系统提供的能力。

真正应该先问的是:

现在发生变化的到底是什么?

如果组件一直存在,只是下面这些属性发生变化:

  • opacity
  • scale
  • rotate
  • translate
  • width
  • height
  • backgroundColor

那么这是典型的属性变化。官方动画性能文档也把这些属性列为属性动画的典型对象。

比如按钮点击后从 1 倍缩放到 0.9 倍:

@Entry
@Component
struct PropertyAnimationDemo {
  @State scaleValue: number = 1;

  build() {
    Column() {
      Button('Scale')
        .scale({
          x: this.scaleValue,
          y: this.scaleValue
        })
        .onClick(() => {
          this.getUIContext().animateTo({
            duration: 200,
            curve: Curve.EaseInOut
          }, () => {
            this.scaleValue =
              this.scaleValue === 1 ? 0.9 : 1;
          });
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

这里按钮没有离开组件树,改变的只是 scale。用 animateTo 很自然。

但下面这种情况不同:

if (this.show) {
  Text('Network connected')
}

show 从 true 变成 false 后,发生的核心事件已经不只是“Text 的 opacity 从 1 变成 0”,而是Text 要被删除。

这正是组件内转场的使用场景。官方 FAQ 对 transition 的描述是:主要用于容器组件中的子组件插入和删除时显示过渡动效。

三、为什么状态变量更新的位置会影响动画性能

要理解“一个这么简单的动画为什么还可能掉帧”,还得看一下 animateTo 如何处理状态。

官方动画性能文档明确说明:执行 animateTo 时,会比较动画闭包执行前后的状态,只对差异部分进行动画处理。状态发生变化后,相关节点会被标脏,并进入后续刷新流程。

所以状态更新并不是一个完全没有成本的赋值动作。

例如:

this.show = true;

this.getUIContext().animateTo({
  duration: 300
}, () => {
  this.contentOpacity = 0;
});

这里至少存在两个不同目的的状态变化:

show
→ 决定组件是否存在

contentOpacity
→ 决定已有组件的透明度

如果还要在 onFinish 中:

this.show = false;

一次视觉上的“淡出”,实际上被拆成了组件状态更新、属性动画和动画结束后的再次状态更新。

这并不意味着出现一次这样的代码就必然掉帧,但在频繁触发、组件较复杂或者页面本身负载已经较高时,这些额外刷新就值得检查。

华为官方《优化动画性能》还专门讨论了“多次 animateTo 时统一更新状态变量”:如果多个 animateTo 之间继续更新状态,会产生新的脏节点,可能造成冗余更新;官方建议尽可能避免在多个动画之间插入额外的状态更新。

官方文档自己的性能案例中,不同状态更新组织方式也得到了不同的丢帧指标。这里需要强调,这些数字是华为官方示例场景的数据,不是本文自行测试的数据,不能直接当成业务项目的性能收益。

【建议插图2:官方“多次 animateTo 时统一更新状态变量”章节中的状态更新流程示意,发布时建议按照引用规范自行截图】

四、用 Profiler 不要只盯着 FPS

遇到动画卡顿,更有价值的做法不是凭肉眼判断“这个 API 好像比较慢”,而是看一次 Frame 分析。

DevEco Profiler 提供 Frame 场景分析能力,可以录制卡顿过程中的关键数据并进一步定位丢帧原因。官方性能 FAQ 给出了一批很有用的 Trace 关键字。

对于本文这个场景,可以重点关注:

Trace可以关注什么
H:FlushDirtyNodeUpdate状态变化后标脏组件的刷新
H:CustomNodeUpdate自定义组件刷新
H:CreateTaskMeasure组件测量任务
H:CreateTaskLayout组件布局任务
H:Create[...]组件创建
H:JSAnimation显式动画执行
H:ViewPU.viewPropertyHasChanged状态变量变化及其影响组件数量

这些名称及含义来自华为官方卡顿丢帧分析资料。

如果一个本来只想做“提示信息淡出”的动作,在 Trace 中同时伴随着频繁状态刷新、组件创建、Measure/Layout,那么排查方向就不应该停留在“300ms 动画是不是太长”,而应该继续检查:是不是用属性动画模拟了一个本该由组件转场处理的问题。

【建议插图3:DevEco Profiler Frame 分析中选中一次 Toggle 操作,标出 FlushDirtyNodeUpdate、CreateTaskLayout 和 JSAnimation】

五、把实现改成 transition

对于这个例子,状态其实可以只保留一个:

@Entry
@Component
struct TransitionDemo {
  @State show: boolean = true;

  build() {
    Column({ space: 20 }) {
      Row() {
        if (this.show) {
          Text('Network connected')
            .id('network_tip')
            .fontSize(20)
            .transition(
              TransitionEffect.OPACITY.animation({
                duration: 300,
                curve: Curve.EaseInOut
              })
            )
        }
      }
      .width('100%')
      .height(100)
      .justifyContent(FlexAlign.Center)

      Button('Toggle')
        .onClick(() => {
          this.show = !this.show;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

真正需要关注的只有两处。

一处是:

if (this.show)

它表达业务状态:这个组件到底应不应该存在。

另一处是:

.transition(
  TransitionEffect.OPACITY.animation({
    duration: 300,
    curve: Curve.EaseInOut
  })
)

它表达视觉规则:这个组件插入或者删除时,应该怎样过渡。

华为官方 Code Linter 的正例就是这种写法:show 直接取反,组件自身配置 TransitionEffect.OPACITY;官方示例同时给组件设置 id,用于使转场支持打断。

这样业务状态和动画状态就不需要人为拆成:

show
opacity
onFinish
再次修改 show

而变成:

show
  ↓
组件插入 / 删除
  ↓
transition 负责转场

官方性能规则已经直接将这种改法定义为组件转场动画场景的推荐方向。

【建议插图4:animateTo 版本与 transition 版本的状态变量和调用流程对比图】

六、如果还想同时做位移和透明度

transition 并不只能做淡入淡出。

官方当前示例使用 TransitionEffect.OPACITY 配合 combine() 组合其他转场效果,例如旋转;侧边栏官方 FAQ 也展示了 TransitionEffect.OPACITY 与 TransitionEffect.move() 组合使用。

比如提示信息希望从下方轻微移入,可以按照同样的组合思路配置:

.transition(
  TransitionEffect.OPACITY
    .animation({
      duration: 300,
      curve: Curve.EaseInOut
    })
    .combine(
      TransitionEffect.move(TransitionEdge.BOTTOM)
    )
)

这里仍然没有引入额外的 translateY 状态。

如果需求的本质是“组件进入/退出时怎么动”,让转场效果描述它通常更直接。

七、animateTo 和 transition 到底怎么选

可以把边界压缩成一个判断:

场景更应该考虑
已存在组件透明度从 0.5 变成 1animateTo
已存在组件缩放animateTo
已存在组件旋转animateTo
已存在组件平移animateTo
已存在组件宽高变化属性动画,但要关注布局开销
if 控制组件插入transition
if 控制组件删除transition
列表项被插入/删除并需要过渡transition
一个组件消失、另一个组件出现优先从转场问题分析

这不是说两套能力绝对不能组合。官方侧边栏案例本身就同时使用了 animateTo 和 transition:主界面的属性变化由显式动画控制,而真正插入/删除的侧边栏组件配置转场。

所以更准确的理解应该是:

animateTo 描述“已有组件的属性从 A 变成 B”;transition 描述“组件进入或离开组件树时怎么过渡”。

八、还有一个容易忽略的性能点:布局属性和图形变换属性

即使场景确定应该使用 animateTo,也不代表属性可以随便选。

官方动画性能指导区分了布局属性和图形变换属性。修改 width、height、position 等布局相关属性可能带来重新布局;如果视觉目标可以通过 scale、translate、rotate 等图形变换完成,官方建议优先考虑图形变换方式,以减少不必要的布局计算和绘制。

例如只是想让卡片视觉上缩小:

.scale({
  x: this.scaleValue,
  y: this.scaleValue
})

通常比为了同样的视觉效果反复修改:

.width(this.cardWidth)
.height(this.cardHeight)

更值得优先考虑。

这和 transition 的问题其实属于同一类优化思路:先把动画语义选对,再谈动画参数。

九、版本、模型和工程配置说明

本文使用的是 ArkTS 声明式 ArkUI 能力,核心涉及 @State、if 条件渲染、UIContext.animateTo()、组件通用属性 transition()、TransitionEffect、opacity、scale 等。

截至本文检索官方资料时,华为最新动画性能文档仍在使用 this.getUIContext().animateTo(...) 组织显式动画,并明确提供 transition 替换不合适 animateTo 转场实现的 Code Linter 性能规则。

getUIContext() 相关官方 FAQ 同时说明其使用受 Stage 模型约束,因此本文最小示例按 Stage 模型应用场景理解。

本文没有调用需要额外授权的系统服务,因此示例不增加 module.json5 权限配置,也不涉及 HAR、HSP、Native/C++ 或三方依赖。

关于 API Level,这里不做未经当前 API Reference 完整 @since 字段确认的数字推断。华为最新文档中心已经采用覆盖多 API 版本的统一文档,并提供 API 版本筛选;本文确认的是上述接口目前仍在官方最新 ArkUI 文档、FAQ、性能指导及 Code Linter 规则中使用,而不是根据历史印象给它们补写一个首次支持版本。正式落到存量工程时,仍建议根据工程的 compatibleSdkVersion 在 API Reference 中切换到对应版本再次核对。

这比简单写一句“HarmonyOS 7 都支持”更稳妥:版本判断应该落到实际工程使用的 API Level,而不是只看系统大版本名称。

十、实际项目中可以这样排查

遇到“动画明明很简单却不流畅”,建议按这个顺序检查:

  1. 先判断动画语义。 是已有组件属性变化,还是组件插入/删除?
  2. 出现/消失先检查 transition。 如果正在使用 show + opacity + animateTo + onFinish 模拟组件删除,可以重点检查 @performance/hp-arkui-use-transition-to-replace-animateto。
  3. 属性动画检查状态更新位置。 避免在多个 animateTo 之间插入不必要的状态更新,也不要把相同参数的多个属性动画无意义拆成多个闭包。
  4. 检查是否在动画布局属性。 视觉效果能够用 scale、translate、rotate 完成时,可以优先评估图形变换属性。
  5. 最后用 Profiler 看 Frame。 关注状态刷新、Measure、Layout、组件创建和 JSAnimation,不要只凭动画观感判断原因。

开发经验总结

这个问题最有价值的地方,不是记住“transition 比 animateTo 好”,因为这种结论本身就是错的。

真正需要记住的是动画能力的边界。

animateTo 很适合描述一个已经存在的组件,其可动画属性从起始状态变化到目标状态;但当业务状态控制的是组件本身的插入和删除时,再人为维护 opacity、显示状态和动画完成回调,就可能制造额外的状态更新和刷新流程。

transition 则把组件出现/消失这件事直接表达成转场。

另外,动画性能问题也不要只看动画时长。状态变量在什么时候更新、一次交互触发多少次节点刷新、有没有不必要的 Measure/Layout、能否用图形变换代替布局变化,这些往往更值得从 Profiler 里确认。

如果自己的页面里存在大量这样的代码:

show = true
→ animateTo 修改 opacity
→ onFinish
→ show = false

可以把它作为一次动画代码巡检的起点。这里很可能不是“动画参数还没调好”,而是组件出现/消失与属性变化的边界没有分清。

参考资料

  1. 《@performance/hp-arkui-use-transition-to-replace-animateto》
    华为开发者联盟
    链接:华为官方 Code Linter 性能规则:组件转场动画使用 transition

  2. 《优化动画性能》
    华为开发者联盟
    链接:华为官方 ArkUI 动画性能优化文档

  3. 《卡顿丢帧分析》相关官方 FAQ
    华为开发者联盟
    链接:华为官方卡顿丢帧分析资料

  4. 《文字翻转有延迟》
    华为开发者联盟
    链接:华为官方 transition 替代不合适 animateTo 的案例

  5. 《如何实现页面内容随着侧边栏弹出平移》
    华为开发者联盟
    链接:华为官方组件内转场案例

  6. 《@performance/hp-arkui-use-scale-to-replace-attr-animateto》
    华为开发者联盟
    链接:华为官方图形变换属性动画性能规则

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐