目录

Optimizing Animation Performance

Overview

Animation is an indispensable part in app development. It makes apps more vivid and easy to interact. On one hand, animation can improve user experience and enhance visual attractiveness. On the other hand, animation can guide user operations and improve data transmission efficiency. In an app, animation can be added for page transition, tap interaction, and gesture control. You can use animation properly from the following two aspects:

When using animation, you need to design and use it properly according to the scenario and user requirements, and care about the performance impact of animation, and take optimization measures in time. Proper use of animation can improve the perceptual and running smoothness of animation, which offers attractive user experience and high performance of apps.

Improving the Smoothness of Animation Perception

HarmonyOS gravity-inspired motion system complies with the original design concept to create a natural, smooth, and high-quality operation experience. Based on the visual effect design, animations can be classified into feature animation, transition animation, gesture animation, micro animation, and illustration animation. Feature animation presents the visual sense of the celestial movement "force". Transition animation shows the order of the "force" during the movement of an object. Gesture animation creates the sense of control in which element movements affect each other's "force". Micro animation and illustration animation empower HarmonyOS gravity-inspired motion system, making operations more interesting and browsing more enjoyable. Animation should always be centered on the operation in line with the user's expectations, the movement of objects in line with the real world, and the element expression form which highlights the brand and tonality of the product, and improves the smoothness from the perspective of user perception.

HarmonyOS provides rich animation capabilities. In the actual development process, you need to convert the preceding UX design perspective to the development implementation perspective, that is, use the animation capabilities provided by HarmonyOS to implement the UX design scenario and animation. Generally, you need to perform the following steps to convert the view angle:

  1. Understand system capabilities. Deeply understand the animation capabilities provided by HarmonyOS, including animation types and animation-related APIs.
  2. Analyze the UX design perspective. Carefully analyze the animation provided by the UX design to understand the designer's intent.
  3. Design an animation scheme. Design an appropriate animation scheme based on the analysis result. Determine the time of triggering animation, the type and parameters of animation, and the like.
  4. Use animation capabilities. Use the animation capabilities provided by HarmonyOS, such as property animation and path animation, or call third-party libraries to complete the design.
  5. Debug and optimize animation. Debug and optimize animation during implementation. Ensure that animation is smooth, meets the expectation, and fulfill the performance requirements.

By performing the preceding steps, you can convert the UX design perspective into the development implementation perspective and convert the animation provided by the designer into specific code implementation. This ensures that the app achieves the desired effect of the design in actual use, improves the smoothness of animation perception, and provides a good user experience.

Figure 1 Proper use of animations

[h2]Designing Animation Scenarios

When designing animation, you need to clearly understand its functions in the system. Animation reflects the smooth transition of pages, clear prompts of objects, element hierarchy, and product image.

Feature Animation

Feature animation is mainly to create "the perception of celestial objects", which provides an interactive experience for the brand effect of celestial objects and the sense of cosmic space. It imparts force to elements and intuitively conveys visual, materialized, and dynamic design concepts. It expresses novel personality in different scenes and highlights the unique product tonality. It can be widely used in scenarios such as opening, loading, and downloading animation.

Feature animation refers to the animation effect that highlights a specific element in the UI. It can attract users' attention and improve user experience. For example, in an app, when a user taps the Download button, a progress bar is displayed in a gradient manner and an item is dynamically loaded (as shown in the following figure).

Figure 2 Feature animation triggered by a tap

Transition

A transition animation is an animation effect used when switching between different pages or views. With transition animation, you can smoothly transition to the next page or view, increasing the continuity and smoothness between pages.

HarmonyOS provides a rich transition animation library for you to easily achieve various transition animation effects. You can apply these transition animation effects in different scenarios of the app based on specific requirements to improve user experience and UI attractiveness. It should be noted that for the best user experience, you should select proper transition animations according to the functions and features of the UI, and follow the guidelines for using the transition animation to ensure the visual and interactive consistency of the transition animation effect.

Figure 3 Transition animation

Gesture Animation

Gesture animations refer to the animation effects generated based on users' gesture operations. It can enhance the interaction experience between the user and the device.

Figure 4 Gesture animation

Micro Animation

A micro animation refers to the subtle animation effect on the UI. It is used to improve the vividness and interactivity of the UI. A micro animation may be reflected in a tap effect of a button, an icon change, a text display, and the like. For example, when a user opens a panel, the panel may be represented by using a tiny zoom or color change, as shown in the figure.

Figure 5 Micro animation

Illustration Animation

An illustration animation refers to the illustration-based animation effect applied on the UI. It can be used to add fun and personalization to the UI. For example, in a game app, illustration animation can be used to display an action or an expression of a character, or a scene change, as shown in the figure.

Illustration animations can enrich the information to be expressed by visual elements, guide the interpretation of functional information, and connect neighboring screens to make them appear seamless. This enables users to understand the information and makes the picture performance more vibrant.

Figure 6 Illustration animation

[h2]Selecting Animation Capabilities

After receiving design requirements, you need to select appropriate animation capabilities to complete the design. HarmonyOS provides three methods: system capabilities, resource invoking, and third-party libraries. When selecting animation capabilities, you need to specify objectives and requirements, properly select tools that can meet requirements; and pursue high efficiency and high quality, to empower apps for better animation effects.

System Capabilities

Resource Invoking

Third-Party Libraries

[h2]Animation Practice Cases

Using an Explicit Animation to Achieve the Feature Animation

Using a Keyframe Animation to Implement the Micro Animation

Using a Property Animation to Achieve the Gesture Animation

[h2]Summary

During app development, animation can add vivid and smooth interaction effects to the UI, improving user experience in apps. However, abuse of animation will deteriorate app performance, consume excessive system resources, and even compromise user experience.

This topic describes how to fully utilize the animation capabilities provided by HarmonyOS in specific design scenarios to ensure that animation can enhance user experience without deteriorating app performance. When implementing animation effects, you need to control the complexity and frequency of animation to save system resources and improve app performance. To improve the smoothness of animation perception, consider the following points:

Proper use of animation capabilities provided by HarmonyOS can effectively improve user experience and attractiveness of apps. During design and implementation, however, you need to consider the scenario, frequency, and impact of animation on system resources, and take advantage of strengths and avoid weaknesses, to ensure the performance and stability of apps.

Improving the Smoothness of Animation Running

Animation is crucial to app development. It can improve user experience, convey information, guide user operations, improve app quality, and increase visual attractiveness. The performance of animation is also critical. It can be optimized from several aspects such as property update and layout to minimize redundant refresh. This topic describes the following five methods for optimization. You can use one or more of these methods to optimize the animation frame rate and app frame freezing, improving performance and user experience.

The following describes how to verify the benefits of the preceding optimization methods through five tests to prove the feasibility of these methods. Based on the common metrics in the industry and actual user experience, the following key metric items are compared and analyzed in the tests:

Test results demonstrate that when system-provided APIs can deliver the required animation effects, they significantly outperform custom animation implementations. Custom animations, which involve substantial computations on the app side, often result in significant frame drops. In contrast, system-provided APIs, which do not rely on app-side computations, offer smoother performance. For animations involving layout changes (such as translation, scaling, and rotation), using transform properties instead of layout properties can reduce the frame drop rate from 8.1% to 3.7%, significantly decreasing the number of severe frame freezes. Additionally, consolidating multiple animation closures with identical parameters into a single closure can lower the frame drop rate from 9.6% to 4.0%. Similarly, avoiding state updates between concurrent animation closures can further reduce animation-related frame loss.

[h2]Using System-Provided Animation APIs

Generally, in HarmonyOS app development, animation can be designed by using custom animation or system-provided animation APIs.

Scenario Design

To simultaneously zoom in or out multiple buttons on the same page (as shown in the following figure), use custom animation, property animation, and explicit animation, respectively.

Figure 10 Scaling multiple buttons on the same screen at the same time

Custom Animation

When playing an animation, the system needs to complete operations, such as calculating an animation curve and drawing the component layout, within a refresh period. The following uses a custom animation. The process of calculating an animation curve may cause high load of UI threads, which is prone to frame loss.

Use custom animation to implement key scaling. The code is as follows:

@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)
  }
}

System-Provided Animation APIs

With system-provided animation APIs, you can set only items such as curve type, end position, and duration to achieve common animation functions and reduce the load of the UI main thread.

Performance Analysis

The Profiler tool of DevEco Studio is used to check the following metrics. The obtained data is as follows.

Table 1 Comparison of indicators for implementing the same scenario using custom animation, property animation, and explicit animation

Indicator Custom Animation Property Animation Explicit Animation
Frame loss rate (%) 9.2% 0% 0%
Maximum number of consecutive lost frames 7 0 0
Average number of stuttering times per second 1.50 0 0

Conclusion:

  1. When custom animation is used to implement an animation with a large amount of calculation, the frame loss rate reaches 9.2%, the maximum number of consecutive lost frames reaches 7, and the average number of stuttering times per second reaches 1.5.

  2. When the system animation APIs are used to implement the same effect, both the property animation API and explicit animation API can achieve zero frame loss.

The result shows that the internal implementation of system-provided animation APIs is transparent and the performance has been maximized to avoid performance deterioration when you use custom animations. Custom animation is suitable for achieving unique and complex animation effects. System-provided animation APIs are suitable for achieving common animation effects and better performance. Therefore, if animation can be achieved using system-provided APIs, you should use these APIs if possible to ensure smoothness and stability of the animation, thereby enhancing the app's performance.

[h2]Using Graphic Transformation Properties to Adjust Component Layout

During app development, the layout display of components can be changed in two methods: changing layout properties; and changing graphic transformation properties.

The following table lists the relationship for replacement between layout properties and graphic transformation properties.

Table 2

Graphic Transformation Property Layout
rotate /
translate position, offset
scale width, height, Size
transform /

The mapping in the table is for reference only. You can explore more replacement relationships.

Scenario Design

If multiple images on the same page are to be zoomed and displaced simultaneously (as shown in the following figure), you can change the layout property and graphic transformation property.

Figure 11 Scaling and moving multiple images on the same screen at the same time

Changing Layout Properties

When a layout property changes, the system needs to recalculate the position and size of a component and then re-lay out the component. This process consumes a large amount of computing resources and time. Especially when the UI involves a large number of components or complex layout, the performance overhead is more serious.

The width, height, and position properties of a component are used to change the size and position of the component. The code is as follows:

@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)
  }
}

Changing Graphic Transformation Properties

Graphic transformation only changes the display effect of a component, but does not change the position and size of the component in the layout. Therefore, it does not trigger the re-layout calculation process. In this way, smoothness and performance of the UI can be improved when the animation effect is implemented by using the graphic transformation property.

The scale and translate properties of a component are used to change the size and position of the component. The code is as follows:

@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)
  }
}

Performance Analysis

The Profiler tool of DevEco Studio is used to check the following metrics. The obtained data is as follows.

Table 3 Comparison of indicators in the same scenario when the layout attributes and graphic transformation attributes are changed

Indicator Changing Layout Properties Changing the Graphic Transformation Properties
Frame loss rate (%) 8.1% 3.7%
Maximum number of consecutive lost frames 8 2
Average number of stuttering times per second 0.48 0

Conclusion:

  1. When the layout properties are used to change the image size and position, the frame loss rate is 8.1%, a maximum of eight consecutive frames are lost, and the average number of stuttering times per second is 0.48.

  2. When the graphic transformation properties are used to change the image size and position, the frame loss rate decreases to 3.7%, a maximum of two consecutive frames are lost, and no stuttering occurs within the same period.

The results show that the frame loss rate and the occurrence frequency of large frame freezing can be significantly reduced when the image size and position are changed by using the graphic transformation property. The UI layout is time-consuming. For this reason, frequent modification of layout properties will cause UI performance deterioration and frame freezing, which compromises user experience. Therefore, if animation can be achieved using graphic transformation properties, you should use these properties if possible to ensure smoothness and stability of the animation and improve the performance of apps.

[h2]Using the Same animateTo Method When the Parameters Are the Same

You are advised to use the Code Linter to check code, especially the follow @performance/hp-arkui-combine-same-arg-animateto rules. If issues related to this rule are identified in the scan results, adjust your code by following the optimization suggestions provided in this section.

Each call for the animateTo method will cause a property change. This means that each animation execution requires a comparison before and after the animation to determine the property change. Multiple consecutive calls for animateTo will cause additional layout calculation and drawing overhead, which degrades the performance. Especially when these animateTo operations are performed on a property of a same component, the number of updates to the component increases, which further compromises the performance.

In actual development, if multiple properties need to be changed with the same animation parameters, you are advised to put them in the same animation closure for execution. By combining the animation operations of multiple properties into the same animation closure, you can reduce the number of updates to components, avoid repeated layout calculation and drawing operations, and improve the performance of animation effects.

In addition to the performance benefits, merging animation operations for multiple properties into the same animation closure helps improve code readability and maintainability. Centralized management of associated property changes makes the code structure clearer and facilitates subsequent maintenance and modification.

Scenario Design

If multiple properties of multiple same components are to be modified simultaneously (as shown in the following figure), multiple animateTo closures and one animateTo closure are used, respectively.

Figure 12 Modifying multiple properties of the same component at the same time

Code Implementation

Performance Analysis

The Profiler tool of DevEco Studio is used to check the following metrics. The obtained data is as follows.

Table 4 Comparison of indicators when multiple animateTo closures and one animateTo closure are used to implement the same scenario

Indicator Multiple animateTo Closures One animateTo Closure
Frame loss rate (%) 9.6% 4.0%
Maximum number of consecutive lost frames 9 4
Average number of stuttering times per second 1.01 0.24

Conclusion:

  1. When the four property change animations are placed in four animateTo animation closures, the frame loss rate is 9.6%, with a maximum of 9 consecutive lost frames, and the average number of stuttering times per second is 1.01.

  2. When the four property change animations are placed in one animateTo animation closure, the frame loss rate decreases to 4.0%, with a maximum of 4 consecutive lost frames, and the average number of stuttering times per second is 0.24.

The results show that combining the animation with multiple property changes into one animateTo animation closure can significantly reduce the frame loss rate and the occurrence frequency of large freezing. Combining animation can reduce unnecessary layout calculation and drawing overhead, which improves the smoothness and performance of the animation and optimizes the performance of animation effects.

[h2]Uniformly Updating State Variables for Multiple animateTo Calls

When the animateTo method is used to execute animation, the state before and after the animation closure is compared, and then only the differences are animated.

In an animation execution process, a dirty node is an area that needs to be redrawn on the UI. If the state has changed, ArkUI tracks the changes and compares the state before the animation closure is executed. Related dirty nodes are marked as to be refreshed so that they can be redrawn before the animation closure is executed. In this way, only the changed parts are included in the animation process, without the need to redraw the entire UI.

This difference comparison method can significantly reduce unnecessary drawing operations, and improve the performance and smoothness of animation.

Scenario Design

If multiple properties of multiple same components are to be modified for animation (as shown in the following figure), you can update state variables respectively among multiple animateTo calls, explicitly specify the initial property values before the animateTo calls, and use the original states before the animateTo calls.

Figure 13 Modifying multiple properties of multiple identical components

Updating State Variables Among animateTo Calls

If state variables are updated among animateTo calls, dirty nodes that need to be updated will occur before the next animateTo call, which may cause redundant updates. The following figure shows the animation update process.

Figure 14 Process of updating state variables between multiple animateTo operations

The w property is reset before the first animateTo call. Therefore, the Row component needs to be updated. In the animation closure of the first animateTo call, the w property is changed. Therefore, the Row component needs to be updated again and compared with the last update result to generate a width and height animation. Before the second animateTo call, the color property is changed. Therefore, the Row component needs to be updated again. In the animation closure of the second animateTo call, the color property is changed. Therefore, the Row component needs to be updated again to generate a background color animation. In sum, the Row component is updated four times for its property changes.

In addition, the state h irrelevant to the animation is changed. If the state does not need to be changed, it should not be changed to cause redundant updates.

The code is as follows:

@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)
  }
}

Uniformly Updating State Variables

Before the first animateTo call, the w and color properties are changed. Therefore, the Row component needs to be updated again. In the animation closure of the first animateTo call, the w property is changed. Therefore, the Row component needs to be updated and compared with the last update result to generate a width and height animation. Before the second animateTo call, because no additional statement is executed, there is no dirty state variable or dirty node that needs to be updated, and no update is required. In the animation closure of the second animateTo call, the color property is changed. Therefore, the Row component needs to be updated again to generate a background color animation. In sum, the Row component is updated three times for its property changes.

The code is as follows:

@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)
  }
}

Performance Analysis

The Profiler tool of DevEco Studio is used to check the following metrics. The obtained data is as follows.

Table 5 Comparison of indicators with different locations for updating state variables

Indicator Update State Variables Between Multiple animateTo Calls Explicitly Specify the Initial Value of a Property Before animateTo Use the Original State Before animateTo
Frame loss rate (%) 9.1% 5.4% 3.5%
Maximum number of consecutive lost frames 15 10 5
Average number of stuttering times per second 0.67 0.23 0.10

Conclusion:

  1. When the state variable is updated between multiple animateTo calls, the frame loss rate reaches 9.1%, with a maximum of 15 consecutive frames lost and an average of 0.67 stuttering times per second.

  2. When the initial value of the attribute is explicitly specified before the animateTo call, the frame loss rate decreases to 5.4%, with a maximum of 10 consecutive frames lost and an average of 0.23 stuttering times per second.

  3. When the original state is used before the animateTo call, the frame loss rate decreases to 3.5%, with a maximum of 5 consecutive frames lost and an average of 0.10 stuttering times per second.

During property change animation, explicitly specifying the initial value of a property or using the original state as the start state of the animation can significantly reduce the frame loss rate and the frequency of large frame freezing exceptions. Therefore, during animation operations, properly managing the update of a state variable and setting of an initial value is critical to optimizing the performance of animation effects. You should avoid updating state variables among multiple animateTo calls if possible based on the requirements, to improve smoothness and performance of the animation.

[h2]Using RenderGroup

renderGroup is a common component method, which represents a combination of rendering and drawing. Its core function is to mark a component. During drawing, the drawing results of the component and its child components are combined and cached for reuse, which reduces the drawing load.

If a component to be drawn for the first time is marked with renderGroup enabled, the component and its child components are drawn off-screen, and the drawing results are saved to the cache. If the component needs to be redrawn, the cache is preferentially used instead of redrawing, which reduces the drawing load and optimizes the rendering performance. The following figure shows the process for rendering a component.

Figure 17 Component rendering process

To update the cache, ensure that the following conditions are met:

To clear the cache, ensure that any of the following conditions is met:

The following figure shows the cache management process.

To enable the renderGroup function to take effect, the component is subject to the following restrictions:

  • Fixed component content: The properties of the parent component and its child components are fixed rather than being changed. If the content of the parent component is not fixed, that is, there are components whose properties or styles change in the child components of the parent component, the cache usage will be seriously reduced when renderGroup is used. In this case, frame freezing may become more serious instead of being rectified. For example, use the dynamic data bound bidirectionally as the text content, use GIF images as the image resource, or use the Video component to play videos.
  • No animation effect for child components: The parent component applies the animation effect in a unified manner, and its child components have no animation effect. Otherwise, the child components are no longer static relative to the parent component. The cache may need to be updated for each frame, which also consumes system resources.

Scenario Design

In this example, fixed images and text content are used on the same page, and each component uses the rotation and scaling effects. In addition, 60 such components are reused.

Figure 18 Implementation of the renderGroup example

Code implementation

Implement the components required on the page, set the renderGroup property, and use @StorageLink to bind renderGroupFlag. The renderGroup property must be set to ensure that the parent component applies the animation effect in a unified manner and its child components have no animation effect. Therefore, the animation effect is achieved on the parent component in a unified manner instead of being specified for IconItem. In the Flex component, Image and Text do not have animation effects. Therefore, you only need to set the renderGroup property in the outermost Flex component.

The code is as follows:

@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)
  }
}

On the displayed page, save the renderGroupFlag flag that controls renderGroup to the APPStorage wrapper.

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)
  }
}

Performance Analysis

The Profiler tool of DevEco Studio is used to check the following metrics. The obtained data is as follows.

Table 6 Comparison of indicators in the scenario where renderGroup is disabled and enabled

Indicator renderGroup Disabled renderGroup Enabled
Frame loss rate (%) 52.3% 0%
Maximum number of consecutive lost frames 36 0
Average number of stuttering times per second 2.3 0

Conclusion:

  1. When renderGroup is disabled, the frame loss rate on the render_service side is 52.3%, with a maximum of 36 consecutive lost frames, and an average of 2.3 stuttering times per second.

  2. When renderGroup is enabled, neither stuttering nor frame loss occurs, and the frame loss rate is 0%.

The result shows that when the preceding constraints are met, the frame loss rate, maximum number of consecutive lost frames, and average number of frame freezing times per second can be significantly reduced if renderGroup is enabled. Therefore, if a large number of components that apply animation effects exist on a single page, properties of these components and their child components are fixed, and animation effects are applied in a unified manner, you can use renderGroup to improve app performance and ensure smoothness of apps and animations.

[h2]Summary

Five tests are performed to verify the effectiveness of the following five measures: using system-provided animation APIs; using the graphic transformation property to change the component layout; using the same animateTo when the parameters are the same; uniformly updating state variables for multiple animateTo calls; using renderGroup.

Animation performance is crucial to a variety of aspects, including user experience, device resource consumption, app stability, and traffic consumption.

In actual development, you can take the four measures provided in this document and optimize the animation based on scenarios and requirements to achieve better animation performance.

Conclusion

In the actual development process, you should fully understand the importance of animation to user experience. Animations not only enhance the visual appeal of apps but also elevate user engagement and streamline the intuitiveness of interactions.

This topic describes how to properly use animation from two perspectives:

In sum, proper use of animation is critical to improving user experience. You need to transform from the UX design perspective to the development implementation perspective, fully understand the functions and significance of animation in user experience, and improve the effect and stability of animation by means of proper implementation and performance optimization.