目录

优化动画性能

概述

动画是应用开发中必不可少的部分,它可以使应用程序更加生动和易于互动,一方面可以提升用户体验、增强视觉吸引力,另一方面可以引导用户操作、提高信息传达效率。应用程序中,页面层级间的转场、点击交互、手势操控都可以添加动画。合理使用动画可以通过以下两个方面考虑:

在使用动画时,需要根据具体场景和用户需求进行合理的设计和运用,并且需要注意动画的性能影响,及时采取相应的优化措施。通过合理使用动画,可以提升动画的感知流畅度和运行流畅度,从而提升应用程序的用户体验和性能。

提升动画感知流畅度

HarmonyOS动效引力体系,围绕回归本源设计理念,打造了自然、流畅、品质一体的操作体验。基于视觉效果设计,可以将动效划分为特征动效、转场动效、手势动效、微动效、插画动效。在特征动效中呈现出天体运动“力”的既视感;转场动效表现出物体在运动过程中“力”的秩序感;手势动效打造出元素运动互相影响“力”的控制感;微动效和插画动效辅助HarmonyOS动效引力体系,增加用户的操作趣味性和浏览愉悦感。动效要始终围绕操作符合用户心理预期,物体运动符合真实世界,元素表现形态凸显产品的品牌与调性,从用户感知角度提升流畅度。

HarmonyOS系统为开发者提供了丰富的动画能力,在实际开发过程中,需要把上述UX设计视角转换为开发实现视角,即使用HarmonyOS系统提供的动画能力来实现UX设计的场景和动效,一般来说需要采取如下步骤完成视角转换:

  1. 了解系统能力:首先,开发者需要深入了解HarmonyOS系统提供的动画能力,包括动画类型和动画相关API。
  2. 分析UX设计视角:仔细分析UX设计所提供的动效,理解设计师的意图。
  3. 设计动画方案:基于分析的结果,设计出合理的动画方案。确定动画的触发时机、动画的类型和参数等。
  4. 使用动画能力:利用HarmonyOS提供的动画能力,如属性动画、路径动画等或者调用三方库,完成设计效果。
  5. 调试和优化:在实施动画的过程中,进行调试和优化。确保动画效果流畅,动效符合预期,且满足性能要求。

通过以上步骤,开发者可以将UX设计视角转换为开发实现视角,并将设计师提供的动效转化为具体的代码实现。这样可以确保应用在实际使用中达到设计的预期效果,提升动画感知流畅度并提供良好的用户体验。

图 1 合理使用动画

[h2]动效场景设计

在设计动效过程中,要清楚地理解动效在系统中承载的作用,动效能体现页面的流畅过渡、对象的明确提示、元素的层级关系、产品的品牌印象等。

特征动效

特征动效主要打造 “天体拟物感知”,提供一种天体拟物的品牌效应和宇宙空间感的交互体验,它将力赋予元素,更直观地传递出形象化、拟物化、动态化的设计理念,在不同场景上表达新颖个性的同时又凸显了独特的产品调性。它可以广泛应用于开场动画、加载动画、下载动画等场景。

特征动效是指在用户界面中突出某个特定元素的动画效果。通过特征动效,可以吸引用户的注意力,提升用户体验。例如,在一个应用程序中,当用户点击”下载”按钮时,渐变显示出进度条并动态加载(如下图所示)。

图 2 点击特征动效示意

转场动效

转场动效是指在不同页面或视图之间切换时使用的动画效果。通过转场动效,可以平滑地过渡到下一个页面或视图,增加界面间的连贯性和流畅性。

HarmonyOS系统为开发者提供了丰富的转场动效库,使开发者能够轻松实现各种转场动画效果。开发者可以根据具体需求,在应用的不同场景中应用这些转场动效,以提升用户体验和界面的吸引力。需要注意的是,为了最佳的用户体验,开发者应根据界面的功能和特点,合理选择转场动效,并遵循动效的使用准则,以确保转场动效在视觉和交互上的一致性。

图 3 转场动效示意

手势动效

手势动效是指根据用户的手势操作而产生的动画效果。通过手势动效,可以增强用户与设备之间的互动体验。

图 4 手势动效示意

微动效

微动效是指在界面中细微的动画效果,用于增加界面的生动感和交互性。微动效可以体现在按钮的点击效果、图标的变化、文本的出现等。例如,当用户打开某个面板时,可以使用微小的缩放或颜色变化来体现(如图所示)。

图 5 微动效示意

插画动效

插画动效是指在界面中应用的基于插画的动画效果。通过插画动效,可以为界面增添趣味和个性化。例如,在一个游戏应用中,可以使用插画动效来展示角色的动作、表情或者场景的变化(如图所示)。

通过动画的方式丰富视觉元素所要表达的信息,可以引导解读功能信息并串联前后画面,便于用户理解,也使画面表现更富有生命力。

图 6 插画动效示意

[h2]动画能力选型

开发人员接收到设计需求后,需要选择合适的动画能力完成该设计。HarmonyOS为开发者提供了系统能力、资源调用、三方库三种方式,在选择动画能力时,开发者需要考虑目标和需求以及效率和质量,合理选择能够满足需求的工具、追求高效率和高质量的结果导向,帮助应用实现更好的动画效果。

系统能力

资源调用

三方库

[h2]动画实践案例

使用显式动画实现特征动效

使用关键帧动画实现微动效

使用属性动画实现手势动效

[h2]提升动画感知流畅度总结

在应用开发中,动画可以为用户界面增添生动、流畅的交互效果,提升用户对应用的好感度。然而,滥用动画也会导致应用性能下降,消耗过多的系统资源,甚至影响用户体验。

本文重点描述了如何在特定设计场景下充分利用HarmonyOS提供的动画能力,以确保动画既能够增强用户体验,又不至于对应用性能造成负面影响。开发者需要在实现动画效果时,注意控制动画的复杂度和频率,以节约系统资源,提升应用的性能表现。提升动画感知流畅度需要考虑以下几点:

合理利用HarmonyOS提供的动画能力可以有效提升应用的用户体验和吸引力,但开发者需要在设计和实现过程中综合考虑动画的使用场景、频率以及对系统资源的影响,扬长避短,确保应用的性能和稳定性。

提升动画运行流畅度

动画在应用开发中扮演着重要的角色,能够提升用户体验,传达信息,引导用户操作,提升应用品质和增加视觉吸引力。而动画的性能表现也至关重要,优化可以从属性更新和布局等几个方面考虑,尽可能减少冗余刷新。本文将介绍如下5种优化手段,通过这些优化手段的单个使用或组合使用,可以对动画帧率、应用卡顿等方面带来优化,提升性能和用户体验:

下文将通过5个测试来验证上述优化方法的收益,以证明这些优化手段的可行性。综合考虑业界共识指标和实际用户使用体验,测试将对比分析如下几个关键指标:

测试表明,当系统提供的接口可以实现所需动画效果时,其性能明显优于使用自定义动画实现,自定义动画大量的计算会导致其在应用侧大量丢帧,而系统提供的接口不依赖应用侧计算,更加流畅顺滑;实现位移、缩放、旋转等布局变化动画时,使用图形变换属性替换布局属性实现动画可以将丢帧率从8.1%降至3.7%,且明显减少了大卡顿次数;而多个参数相同的动画闭包同时实现时,将变化属性放入一个动画闭包可以使丢帧率从9.6%降低至4.0%;同样的,避免在两个动画闭包之间更新状态也可以降低动画的丢帧率。

[h2]使用系统提供的动画接口

一般而言,在HarmonyOS应用开发中,动画设计实现可以通过自定义动画或系统提供的动画接口两种方式来实现。

场景设计

针对同一界面多个按钮同时缩放的场景(如下图所示),分别用自定义动画、属性动画、显式动画实现。

图 10 同一界面多个按钮同时缩放示意图

自定义动画

播放动画时,系统需要在一个刷新周期内完成动画变化曲线的计算,完成组件布局绘制等操作。使用了自定义动画,动画曲线计算过程很容易引起UI线程高负载,易导致丢帧。

使用自定义动画实现按键缩放,具体实现代码如下:

@Component
export struct CustomAnimation {
  @State widthSize: number = 80
  @State heightSize: number = 40
  @State flag: boolean = true

  computeSize() {
    let duration = 2000
    let period = 1
    let widthSizeEnd = 0
    let heightSizeEnd = 0
    if (this.flag) {
      widthSizeEnd = 50
      heightSizeEnd = 25
    } else {
      widthSizeEnd = 80
      heightSizeEnd = 40
    }
    let doTimes = duration / period
    let deltaHeight = (heightSizeEnd - this.heightSize) / doTimes
    let deltaWeight = (widthSizeEnd - this.widthSize) / doTimes
    for (let i = 1; i <= doTimes; i++) {
      let t = period * (i);
      setTimeout(() => {
        this.heightSize = this.heightSize + deltaHeight
        this.widthSize = this.widthSize + deltaWeight
      }, t)
    }
    this.flag = !this.flag
  }

  build() {
    Column() {
      Column() {
        Row() {
          Button()
            .width(this.widthSize)
            .height(this.heightSize)
            .margin(20)
          Button()
            .width(this.widthSize)
            .height(this.heightSize)
            .margin(20)
          Button()
            .width(this.widthSize)
            .height(this.heightSize)
            .margin(20)
        }
        // ...
      }
      .height('85%')

      Column() {
        Button('Tap me')
          .width('100%')
          .onClick(() => {
            setTimeout(() => {
              this.computeSize()
            }, 500)
          })
      }
      .justifyContent(FlexAlign.End)
      .width('100%')
      .height('15%')
    }
    .padding({
      left: 16,
      right: 16,
      bottom: 16
    })
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
  }
}

系统动画接口

系统提供的动画接口,只需设置曲线类型、终点位置、时长等信息,就能够满足常用的动画功能,减少UI主线程的负载。

性能分析

使用DevEco Studio的Profiler工具检测下述指标,得到的数据如下所示:

表 1 自定义动画、属性动画、显式动画实现相同场景的指标对比

指标 自定义动画 属性动画 显式动画
丢帧率(%) 9.2% 0% 0%
最大连续丢帧数(帧) 7 0 0
平均1s大卡顿次数(次/s) 1.50 0 0

分析结论:

  1. 使用自定义动画实现计算量较大的动画时,丢帧率高达9.2%,最大连续丢失7帧,平均每秒发生大卡顿1.5次。

  2. 使用系统动画接口实现相同效果时,属性动画接口和显式动画接口都能够达到0丢帧。

这一结果表明,系统动画接口内部实现对开发者是透明的,已经进行了尽可能的性能优化,避免开发者使用自定义动画时产生不必要的性能劣化。自定义动画适合实现个性化的、复杂的动画效果,而系统提供的动画接口则适合实现常见的动画效果并且能够获得更好的性能表现。因此,在动画能够使用系统提供的接口实现的情况下,开发者应尽量使用系统提供的接口实现,保持动画的流畅性和稳定性,提升应用的性能表现。

[h2]使用图形变换属性变化组件

在应用开发中,改动组件的布局显示可以通过改动布局属性、改动图形变换属性两种方式来实现。

布局属性和图形变换属性的可替换关系如下表所示:

表 2

图形变换属性 布局属性
rotate /
translate position、offset
scale width、height、Size
transform /

表格中的对应关系仅供参考,开发者可以自行发掘更多的替代关系。

场景设计

针对同一界面多个图片同时缩放并位移的场景(如下图所示),分别通过改变布局属性、改变图形变换属性实现。

图 11 同一界面多个图片同时缩放并位移示意图

改变布局属性

布局属性发生变化时,系统需要重新计算组件的位置和大小,然后进行重新布局。这个过程需要消耗大量的计算资源和时间,尤其是在界面中包含大量组件或者复杂布局时,性能开销会更加明显。

通过组件的width、height、position属性来改变组件大小和位置,具体代码实现如下:

@Component
export struct ImageAnimation {
  @State imageWidth: number = 60;
  @State imageHeight: number = 60;
  @State xPosition: number = 0;
  @State yPosition: number = 0;
  @State imageScaleX: number = 1;
  @State imageScaleY: number = 1;
  @State imageTranslateX: number = 0;
  @State imageTranslateY: number = 0;

  build() {
    Column() {
      Row() {
        Row() {
          Image($r('app.media.startIcon'))
            .width(this.imageWidth)
            .height(this.imageHeight)
            .position({ x: this.xPosition, y: this.yPosition })
            .scale({
              x: this.imageScaleX,
              y: this.imageScaleY,
              centerX: 0,
              centerY: 0
            })
            .translate({ x: this.imageTranslateX, y: this.imageTranslateY })
        }.width('33%')
        // ...
      }
      .height('25%')
      // ...
      Column() {
        Button('Change Layout Properties')
          .width('100%')
          .onClick(() => {
            for (let i = 0; i < 10; i++) {
              if (i % 2 == 0) {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageWidth = 100;
                    this.imageHeight = 100;
                    this.xPosition = 12;
                    this.yPosition = 12;
                  })
                }, 1000 * i)
              } else {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageWidth = 60;
                    this.imageHeight = 60;
                    this.xPosition = 0;
                    this.yPosition = 0;
                  })
                }, 1000 * i)
              }
            }
          })
        Button('Change Image Properties')
          .width('100%')
          .margin({ top: 12 })
          .onClick(() => {
            for (let i = 0; i < 10; i++) {
              if (i % 2 == 0) {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageScaleX = 2;
                    this.imageScaleY = 2;
                    this.imageTranslateX = 12;
                    this.imageTranslateY = 12;
                  })
                }, 1000 * i)
              } else {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageScaleX = 1;
                    this.imageScaleY = 1;
                    this.imageTranslateX = 0;
                    this.imageTranslateY = 0;
                  })
                }, 1000 * i)
              }
            }
          })
      }
      .justifyContent(FlexAlign.End)
      .height('25%')
    }
    .padding({
      left: 16,
      right: 16,
      bottom: 16
    })
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
  }
}

改变图形变换属性

图形变换只是对组件的显示效果进行变换,而不会改变其在布局中的位置和大小,因此不会触发重新布局的计算过程。这使得使用图形变换属性来实现动画效果时,能够提升界面的流畅性和性能表现。

通过组件的scale、translate属性来改变组件大小和位置,具体代码实现如下:

@Component
export struct ImageAnimation {
  @State imageWidth: number = 60;
  @State imageHeight: number = 60;
  @State xPosition: number = 0;
  @State yPosition: number = 0;
  @State imageScaleX: number = 1;
  @State imageScaleY: number = 1;
  @State imageTranslateX: number = 0;
  @State imageTranslateY: number = 0;

  build() {
    Column() {
      Row() {
        Row() {
          Image($r('app.media.startIcon'))
            .width(this.imageWidth)
            .height(this.imageHeight)
            .position({ x: this.xPosition, y: this.yPosition })
            .scale({
              x: this.imageScaleX,
              y: this.imageScaleY,
              centerX: 0,
              centerY: 0
            })
            .translate({ x: this.imageTranslateX, y: this.imageTranslateY })
        }.width('33%')
        // ...
      }
      .height('25%')
      // ...
      Column() {
        // ...
        Button('Change Image Properties')
          .width('100%')
          .margin({ top: 12 })
          .onClick(() => {
            for (let i = 0; i < 10; i++) {
              if (i % 2 == 0) {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageScaleX = 2;
                    this.imageScaleY = 2;
                    this.imageTranslateX = 12;
                    this.imageTranslateY = 12;
                  })
                }, 1000 * i)
              } else {
                setTimeout(() => {
                  this.getUIContext().animateTo({ duration: 1000 }, () => {
                    this.imageScaleX = 1;
                    this.imageScaleY = 1;
                    this.imageTranslateX = 0;
                    this.imageTranslateY = 0;
                  })
                }, 1000 * i)
              }
            }
          })
      }
      .justifyContent(FlexAlign.End)
      .height('25%')
    }
    .padding({
      left: 16,
      right: 16,
      bottom: 16
    })
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
  }
}

性能分析

使用DevEco Studio的Profiler工具检测下述指标,得到的数据如下所示:

表 3 改变布局属性、改变图形变换属性实现相同场景的指标对比

指标 改变布局属性 改变图形变换属性
丢帧率(%) 8.1% 3.7%
最大连续丢帧数(帧) 8 2
平均1s大卡顿次数(次/s) 0.48 0

分析结论:

  1. 使用布局属性改变图片大小和位置时,丢帧率为8.1%,最多连续丢失8帧,平均每秒发生大卡顿0.48次。

  2. 使用图形变化属性改变图片大小和位置时,丢帧率降到了3.7%,最多连续丢失2帧,在相同时长内没有发生大卡顿。

这一结果表明,使用图形变化属性改变图片大小和位置时,能够显著降低丢帧率和大卡顿的发生频率。界面布局是非常耗时的操作,因此频繁地改动布局属性会导致界面性能下降,出现卡顿现象,影响用户体验。因此,在动画能够使用图形变化属性实现的情况下,开发者应尽量使用图形变化属性实现,保持动画的流畅性和稳定性,提升应用的性能表现。

[h2]参数相同时使用同一个animateTo

建议开发者优先使用Code Linter扫描工具进行代码检查,重点关注@performance/hp-arkui-combine-same-arg-animateto规则。若扫描结果中出现该规则相关问题,可参考本章节提供的优化建议进行调整。

每次调用animateTo方法,都会触发一次属性变化,这意味着在每次动画执行时都需要进行动画前后的对比,以确定属性的变化情况。当多次连续调用animateTo时,会增加额外的布局计算和绘制开销,从而降低性能表现。特别是当这些animateTo操作针对同一个组件的属性时,会导致该组件更新的次数增加,进一步影响性能。

在实际开发中,如果多个属性需要以相同的动画参数进行变化,推荐将它们放到同一个动画闭包中执行。通过将多个属性的动画操作合并到同一个动画闭包中,可以减少对组件的多次更新,避免重复的布局计算和绘制操作,提升动画效果的性能。

除了性能方面的优势,将多个属性的动画操作合并到同一个动画闭包中还有助于提高代码的可读性和维护性。通过集中管理相关联的属性变化,可以使代码结构更加清晰,便于后续的维护和修改。

场景设计

针对多个相同组件同时修改多个属性的场景(如下图所示),分别使用多个animateTo闭包和一个animateTo闭包实现。

图 12 多个相同组件同时修改多个属性示意图

代码实现

性能分析

使用DevEco Studio的Profiler工具检测下述指标,得到的数据如下所示:

表 4 使用多个animateTo闭包和一个animateTo闭包实现相同场景的指标对比

指标 多个animateTo闭包 一个animateTo闭包
丢帧率(%) 9.6% 4.0%
最大连续丢帧数(帧) 9 4
平均1s大卡顿次数(次/s) 1.01 0.24

分析结论:

  1. 将四个属性变化动画分别放在四个animateTo动画闭包中,丢帧率为9.6%,最多连续丢失9帧,平均每秒发生大卡顿1.01次。

  2. 将四个属性变化动画合入一个animateTo动画闭包中,丢帧率降到了4.0%,最多连续丢失4帧,平均每秒发生大卡顿0.24次。

这一结果表明,将多个属性变化动画合并到同一个animateTo动画闭包中能够显著降低丢帧率和大卡顿的发生频率。合并动画操作可以减少不必要的布局计算和绘制开销,从而提升动画的流畅性和性能表现,有助于优化动画效果的性能。

[h2]多次animateTo时统一更新状态变量

使用animateTo方法执行动画时,会对执行动画闭包前后的状态进行对比,然后只对差异部分进行动画处理。

在动画执行过程中,脏节点是指在界面上需要进行重新绘制的区域。如果状态发生了变化,ArkUI会跟踪这些变化,并在动画闭包执行前进行状态对比,相关的脏节点会被标记为需要刷新,以便在动画闭包执行前对这些脏节点进行重新绘制。这样,只有发生变化的部分才会被纳入动画处理,而不需要重新绘制整个界面。

这种差异对比的方式能够显著减少不必要的绘制操作,提高动画的性能和流畅度。

场景设计

针对多个相同组件修改多个属性进行动画的场景(如下图所示),分别使用在多个animateTo之间更新状态变量、在animateTo之前显式指定属性初值、在animateTo之前使用原始状态实现。

图 13 多个相同组件修改多个属性示意图

在多个animateTo之间更新状态变量

如果多个animateTo之间存在状态更新,会导致执行下一个animateTo之前又存在需要更新的脏节点,可能造成冗余更新,其动画更新流程如下图所示:

图 14 多个animateTo之间更新状态变量更新流程

在第一个animateTo前,重新设置了w属性,所以Row组件需要更新一次。在第一个animateTo的动画闭包中,改变了w属性,所以Row组件又需要更新一次并对比产生宽高动画。第二个animateTo前,重新设置了color属性,所以Row组件又需要更新一次。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了4次属性。

此外还更改了与动画无关的状态h,如果不需要改变无关状态,则不应改变造成冗余更新。

其具体代码实现如下:

@Component
export struct UpdateMultipleProperties {
  @State w: number = 100
  @State h: number = 2
  @State color: Color = Color.Red

  build() {
    Column() {
      Row() {
        Row() {
          Row()
            .width(this.w)
            .backgroundColor(this.color)
            .height(this.h)
        }
        .justifyContent(FlexAlign.Center)
        .width('50%')

        Row() {
          Row()
            .width(this.w)
            .backgroundColor(this.color)
            .height(this.h)
        }
        .justifyContent(FlexAlign.Center)
        .width('50%')
      }
      .height('25%')
      .width('100%')

      // ...
      Column() {
        Button('Tap1')
          .width('100%')
          .margin({ top: 12 })
          .onClick(() => {
            let doTimes = 5;
            for (let i = 0; i < doTimes; i++) {
              setTimeout(() => {
                this.w = 80
                this.h = 4
                this.getUIContext().animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
                  this.w = (this.w === 80 ? 150 : 80);
                });
                this.color = Color.Yellow
                this.getUIContext().animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
                  this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
                });
              }, 2000 * i)
            }
          })
        // ...
      }
      .justifyContent(FlexAlign.End)
      .height('25%')
    }
    .padding({
      left: 16,
      right: 16,
      bottom: 16
    })
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
  }
}

统一更新状态变量

在第一个animateTo之前,重新设置了w和color属性,所以Row需要更新一次。在第一个animateTo的动画闭包中,改变了w属性,所以Row组件需要更新一次并对比产生宽高动画。在第二个animateTo之前,由于没有执行额外的语句,不存在需要更新的脏状态变量和脏节点,无需更新。在第二个animateTo的动画闭包中,改变了color属性,所以Row组件再更新一次并产生了背景色动画。Row组件总共更新了3次属性。

其具体代码实现如下:

@Component
export struct UpdateMultipleProperties {
  @State w: number = 100
  @State h: number = 2
  @State color: Color = Color.Red

  build() {
    Column() {
      Row() {
        Row() {
          Row()
            .width(this.w)
            .backgroundColor(this.color)
            .height(this.h)
        }
        .justifyContent(FlexAlign.Center)
        .width('50%')

        Row() {
          Row()
            .width(this.w)
            .backgroundColor(this.color)
            .height(this.h)
        }
        .justifyContent(FlexAlign.Center)
        .width('50%')
      }
      .height('25%')
      .width('100%')

      // ...
      Column() {
        // ...
        Button('Tap2')
          .width('100%')
          .margin({ top: 12 })
          .onClick(() => {
            let doTimes = 5;
            for (let i = 0; i < doTimes; i++) {
              setTimeout(() => {
                this.w = 80
                this.color = Color.Yellow
                this.getUIContext().animateTo({ curve: Curve.Sharp, duration: 1000 }, () => {
                  this.w = (this.w === 80 ? 150 : 80);
                });
                this.getUIContext().animateTo({ curve: Curve.Linear, duration: 2000 }, () => {
                  this.color = (this.color === Color.Yellow ? Color.Red : Color.Yellow);
                });
                this.h = 5
              }, 2000 * i)
            }
          })
        // ...
      }
      .justifyContent(FlexAlign.End)
      .height('25%')
    }
    .padding({
      left: 16,
      right: 16,
      bottom: 16
    })
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
  }
}

性能分析

使用DevEco Studio的Profiler工具检测下述指标,得到的数据如下所示:

表 5 状态变量更新位置不同的指标对比

指标 在多个animateTo之间更新状态变量 在animateTo之前显式指定属性初值 在animateTo之前使用原始状态
丢帧率(%) 9.1% 5.4% 3.5%
最大连续丢帧数(帧) 15 10 5
平均1s大卡顿次数(次/s) 0.67 0.23 0.10

分析结论:

  1. 在多个animateTo之间更新状态变量时,丢帧率高达9.1%,最大连续丢失15帧,平均每秒发生大卡顿0.67次。

  2. 在animateTo之前显式指定属性初值时,丢帧率降到了5.4%,最多连续丢失10帧,平均每秒发生大卡顿0.23次。

  3. 在animateTo之前使用原始状态时,丢帧率降到了3.5%,最多连续丢失5帧,平均每秒发生大卡顿0.10次。

在进行属性变化动画时,显式指定属性初值或者使用原始状态作为动画的起始状态能够显著降低丢帧率和大卡顿的发生频率。因此,在进行动画操作时,合理管理状态变量的更新和初始值设定对于优化动画效果的性能至关重要,开发者应根据需要,尽可能地避免在多个animateTo之间更新状态变量,从而提升动画的流畅性和性能表现。

[h2]使用renderGroup

renderGroup是组件通用方法,它代表了渲染绘制的一个组合。其核心功能就是标记组件,在绘制阶段将组件和其子组件的绘制结果进行合并并缓存,以达到复用的效果,从而降低绘制负载。

首次绘制组件时,若组件被标记为启用renderGroup状态,将对组件和其子组件进行离屏绘制,将绘制结果进行缓存。此后当需要重新绘制组件时,就会优先使用缓存而不必重新绘制,从而降低绘制负载,优化渲染性能。组件渲染流程图如下所示:

图 17 组件渲染流程图

在进行缓存更新时,需要满足以下三个条件:

在进行缓存清理时,需要满足以下任意条件:

具体缓存管理流程图如下所示:

为了能使renderGroup功能生效,组件存在以下约束。

  • 组件内容固定不变:父组件和其子组件各属性保持固定,不发生变化。如果父组件内容不是固定的,也就是说其子组件中上存在某些属性变化或者样式变化的组件,此时如果使用renderGroup,那么缓存的利用率将大大下降,并且有可能需要不断执行缓存更新逻辑,在这种情况下,不仅不能优化卡顿效果,甚至还可能使卡顿恶化。例如:文本内容使用双向绑定的动态数据;图片资源使用gif格式;使用video组件播放视频。
  • 子组件无动效:由父组件统一应用动效,其子组件均无动效。如果子组件上也应用动效,那么子组件相对父组件就不再是静止的,每一帧都有可能需要更新缓存,更新逻辑同样需要消耗系统资源。

场景设计

此案例在同一个页面下使用了固定的图片和文本内容,并且每个组件统一使用旋转和缩放的动效,并且重复使用了60个这样的组件。

图 18 renderGroup案例实现图

代码实现

实现页面中需要使用的组件,并设置renderGroup属性,用@StorageLink修饰绑定renderGroupFlag标识。renderGroup属性的设置要满足父组件统一应用动效,子组件无动效,所以,动效在父组件上统一实现,未在IconItem上设置动效。在Flex组件中,Image和Text都无动效设置,所以在最外层Flex上设置renderGroup属性即可。

具体代码如下所示:

@Component
export  struct IconItem {
  @StorageLink('renderGroupFlag') renderGroupFlag: boolean = false;
  image: string | Resource = '';
  text: string | Resource = '';

  build() {
    Flex({
      direction: FlexDirection.Column,
      justifyContent: FlexAlign.Center,
      alignContent: FlexAlign.Center
    }) {
      Image(this.image)
        .height(20)
        .width(20)
        .objectFit(ImageFit.Contain)
        .margin({ left: 15 })

      Text(this.text)
        .fontSize(10)
        .fontColor('#182431')
        .margin({ top: 5 })
        .width(50)
        .opacity(0.8)
        .textAlign(TextAlign.Center)
    }
    .backgroundColor('#e3e3e3')
    .width(50)
    .height(50)
    .borderRadius(25)
    .renderGroup(this.renderGroupFlag)
  }
}

实现展示的页面,将控制renderGroup的标识renderGroupFlag通过APPStorage进行存储。

import { IconItem } from './IconItem'

class IconItemSource {
  image: string | Resource = ''
  text: string | Resource = ''

  constructor(image: string | Resource = '', text: string | Resource = '') {
    this.image = image;
    this.text = text;
  }
}

@Component
export struct RenderGroup {
  @State renderGroupFlag: boolean = false;
  private iconItemSourceList: IconItemSource[] = [];

  aboutToAppear() {
    for (let index = 0; index < 20; index++) {
      const numStart: number = index * 3;
      this.iconItemSourceList.push(
        new IconItemSource($r('app.media.app_icon'), `item${numStart + 1}`),
        new IconItemSource($r('app.media.app_icon'), `item${numStart + 2}`),
        new IconItemSource($r('app.media.app_icon'), `item${numStart + 3}`),
      );
    }
  }

  build() {
    Column() {
      Row() {
        Row() {
          Text('Example')
            .fontSize(24)
            .lineHeight(24)
            .fontColor(Color.Black)
            .fontWeight(FontWeight.Bold)
            .margin({ left: 30 })
        }

        Stack({ alignContent: Alignment.End }) {
          Button(this.renderGroupFlag ? 'renderGroup On' : 'renderGroup Off', {
            type: ButtonType.Normal,
            stateEffect: true
          })
            .fontSize(12)
            .borderRadius(8)
            .width(150)
            .height(30)
            .margin({ right: 30 })
            .onClick(() => {
              this.renderGroupFlag = !this.renderGroupFlag;
              AppStorage.setOrCreate('renderGroupFlag', this.renderGroupFlag)
            })
        }
      }
      .height(56)
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      GridRow({
        columns: 6,
        gutter: { x: 0, y: 0 },
        breakpoints: {
          value: ['400vp', '600vp', '800vp'],
          reference: BreakpointsReference.WindowSize
        },
        direction: GridRowDirection.Row
      }) {
        ForEach(this.iconItemSourceList, (item: IconItemSource) => {
          GridCol() {
            IconItem({ image: item.image, text: item.text })
              .transition(
                TransitionEffect.scale({ x: 0.5, y: 0.5 })
                  .animation({ duration: 3000, curve: Curve.FastOutSlowIn, iterations: -1 })
                  .combine(TransitionEffect.rotate({ z: 1, angle: 360 }))
              )
          }
          .height(70)
          .width('25%')
        })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
  }
}

性能分析

使用DevEco Studio的Profiler工具检测下述指标,得到的数据如下所示:

表 6 未开启renderGroup、开启renderGroup相同场景的指标对比

指标 未开启renderGroup 开启renderGroup
丢帧率(%) 52.3% 0%
最大连续丢帧数(帧) 36 0
平均1s大卡顿次数(次/s) 2.3 0

分析结论:

  1. 未开启renderGroup时,在render_service侧的丢帧率为52.3%,最多连续丢失36帧,平均每秒发生大卡顿2.3次。

  2. 开启renderGroup时,既没有发生卡顿,也没有发生丢帧的情况,其丢帧率为0%。

这一结果表明,在满足上述的约束后,使用renderGroup时,能够显著降低丢帧率、最大连续丢帧数和平均1s大卡顿次数。因此,在单一页面上存在大量应用动效的组件,且组件和其子组件各属性保持固定、组件统一应用动效时,开发者可以使用renderGroup来提升应用的性能,保证应用及动画的流畅性。

[h2]提升动画运行流畅度总结

本文通过5个测试验证了使用系统提供的动画接口、使用图形变换属性变化组件布局、参数相同时使用同一个animateTo、多次animateTo时统一更新状态变量、使用renderGroup这五项措施的有效性。

动画的性能表现对于用户体验、设备资源消耗、应用稳定性和流量消耗等方面都具有重要意义:

在实际开发中,开发人员可以结合本文提出的四项措施,并根据具体场景和需求进行优化,从而达到更好的动画性能表现。

合理使用动画总结

在实际开发过程中,开发者应该充分认识到动画对用户体验的重要性。动画不仅可以增强应用的吸引力,还能够提升用户的参与感和操作的直观性。

本文通过两个角度介绍了如何合理使用动画:

总之,合理使用动画对于提升用户体验至关重要。开发者需要从UX设计视角转换为开发实现视角,充分认识动画在用户体验中的作用和意义,并通过合理的实现和性能优化来提升动画的效果和稳定性。