一、屬性動畫概述

屬性接口(以下簡稱屬性)包含尺寸屬性、佈局屬性、位置屬性等多種類型,用於控制組件的行為。針對當前界面上的組件,其部分屬性(如位置屬性)的變化會引起UI的變化。添加動畫可以讓屬性值從起點逐漸變化到終點,從而產生連續的動畫效果。為保障動畫起點和終點的正確性,屬性動畫會將當前在標髒隊列內的所有節點進行刷新。如果發現當前動畫時長較長時,需要確認當前是否有額外的節點刷新。根據變化時是否能夠添加動畫,可以將屬性分為可動畫屬性和不可動畫屬性。判斷一種屬性是否適合作為可動畫屬性主要有兩個標準:

  1. 屬性變化能夠引起UI的變化。例如,enabled屬性用於控制組件是否可以響應點擊、觸摸等事件,但enabled屬性的變化不會引起UI的變化,因此不適合作為可動畫屬性。
  2. 屬性在變化時適合添加動畫作為過渡。例如,focusable屬性決定當前組件是否可以獲得焦點,當focusable屬性發生變化時,應立即切換到終點值以響應用户行為,不應該加入動畫效果,因此不適合作為可動畫屬性。
1.1 屬性接口分類説明:

可動畫屬性:

  • 系統可動畫屬性:

分類

説明

佈局屬性

位置、大小、內邊距、外邊距、對齊方式、權重等。

仿射變換

平移、旋轉、縮放、錨點等。

背景

背景顏色、背景模糊等。

內容

文字大小、文字顏色,圖片對齊方式、模糊等。

前景

前景顏色等。

Overlay

Overlay屬性等。

外觀

透明度、圓角、邊框、陰影等。

...

...

  • 自定義可動畫屬性:通過自定義屬性動畫機制抽象出的可動畫屬性。

不可動畫屬性:zIndex、focusable等。

通常,可動畫屬性的參數數據類型必須具備連續性,即可以通過插值方法來填補數據點之間的空隙,達到視覺上的連續效果。但屬性的參數數據類型是否能夠進行插值並非決定屬性是否可動畫的關鍵因素。例如,對於設置元素水平方向佈局的direction屬性,其參數數據類型是枚舉值。但是,由於位置屬性是可動畫屬性,ArkUI同樣支持在其屬性值改變引起組件位置變化時添加動畫。

對於可動畫屬性,系統不僅提供通用屬性,還支持自定義可動畫屬性。

  • 系統可動畫屬性:組件自帶的支持改變UI界面的屬性接口,如位置、縮放、模糊等。
  • 自定義可動畫屬性:ArkUI提供@AnimatableExtend裝飾器用於自定義可動畫屬性。開發者可從自定義繪製的內容中抽象出可動畫屬性,用於控制每幀繪製的內容,如自定義繪製音量圖標。通過自定義可動畫屬性,可以為ArkUI中部分原本不支持動畫的屬性添加動畫。

二、實現屬性動畫

通過可動畫屬性改變引起UI上產生的連續視覺效果,即為屬性動畫。屬性動畫是最基礎易懂的動畫,ArkUI提供三種動畫接口animateTo、animation和keyframeAnimateTo驅動組件屬性按照動畫曲線等動畫參數進行連續的變化,產生屬性動畫。

説明 本章節討論的屬性動畫不是狹義的屬性動畫接口,而是通過給定新的可動畫屬性終值,對屬性產生動畫的方式。

動畫接口

作用域

原理

使用場景

animateTo

閉包內改變屬性引起的界面變化。

作用於出現消失轉場。

通用函數,對閉包前界面和閉包中的狀態變量引起的界面之間的差異做動畫。

支持多次調用,支持嵌套。

適用對多個可動畫屬性配置相同動畫參數的動畫。

需要嵌套使用動畫的場景。

animation

組件通過屬性接口綁定的屬性變化引起的界面變化。

識別組件的可動畫屬性變化,自動添加動畫。

組件的接口調用是從下往上執行,animation只會作用於在其之上的屬性調用。

組件可以根據調用順序對多個屬性設置不同的animation。

keyframeAnimateTo

多個閉包內改變屬性引起的分段屬性動畫。

通用函數,每一段閉包中的狀態變量與前一次的差異做動畫。

支持多次調用,不推薦嵌套。

適用於同一屬性需要做連續多個動畫的場景。

2.1 使用animateTo產生屬性動畫
animateTo(value: AnimateParam, event: () => void): void

animateTo接口參數中,value指定AnimateParam對象(包括時長、Curve等)event為動畫的閉包函數,閉包內變量改變產生的屬性動畫將遵循相同的動畫參數。

説明 直接使用animateTo可能導致UI上下文不明確的問題,建議使用getUIContext()獲取UIContext實例,並使用animateTo調用綁定實例的animateTo。

效果圖

HarmonyOS:屬性動畫_鴻蒙

示例代碼

import { curves } from '@kit.ArkUI';

@Entry
@Component
struct AnimateToDemo {
  @State animate: boolean = false;
  // 第一步: 聲明相關狀態變量
  @State rotateValue: number = 0; // 組件一旋轉角度
  @State translateX: number = 0; // 組件二偏移量
  @State opacityValue: number = 1; // 組件二透明度

  // 第二步:將狀態變量設置到相關可動畫屬性接口
  build() {
    Row() {
      // 組件一
      Column() {
      }
      .rotate({ angle: this.rotateValue })
      .backgroundColor('#317AF7')
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .borderRadius(30)
      .onClick(() => {
        this.getUIContext()?.animateTo({ curve: curves.springMotion() }, () => {
          this.animate = !this.animate;
          // 第三步:閉包內通過狀態變量改變UI界面
          // 這裏可以寫任何能改變UI的邏輯比如數組添加,顯隱控制,系統會檢測改變後的UI界面與之前的UI界面的差異,對有差異的部分添加動畫
          // 組件一的rotate屬性發生變化,所以會給組件一添加rotate旋轉動畫
          this.rotateValue = this.animate ? 90 : 0;
          // 組件二的透明度發生變化,所以會給組件二添加透明度的動畫
          this.opacityValue = this.animate ? 0.6 : 1;
          // 組件二的translate屬性發生變化,所以會給組件二添加translate偏移動畫
          this.translateX = this.animate ? 50 : 0;
        })
      })

      // 組件二
      Column() {

      }
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .backgroundColor('#D94838')
      .borderRadius(30)
      .opacity(this.opacityValue)
      .translate({ x: this.translateX })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
2.2 使用animation產生屬性動畫

相比於animateTo接口需要把要執行動畫的屬性的修改放在閉包中,animation接口無需使用閉包,把animation接口加在要做屬性動畫的可動畫屬性後即可。animation只要檢測到其綁定的可動畫屬性發生變化,就會自動添加屬性動畫,animateTo則必須在動畫閉包內改變可動畫屬性的值從而生成動畫。

效果圖

HarmonyOS:屬性動畫_HarmonyOS_02

示例代碼

import { curves } from '@kit.ArkUI';

@Entry
@Component
struct AnimationDemo2 {
  @State animate: boolean = false;
  // 第一步: 聲明相關狀態變量
  @State rotateValue: number = 0; // 組件一旋轉角度
  @State translateX: number = 0; // 組件二偏移量
  @State opacityValue: number = 1; // 組件二透明度

  // 第二步:將狀態變量設置到相關可動畫屬性接口
  build() {
    Row() {
      // 組件一
      Column() {
      }
      .opacity(this.opacityValue)
      .rotate({ angle: this.rotateValue })
      // 第三步:通過屬性動畫接口開啓屬性動畫
      .animation({ curve: curves.springMotion() })
      .backgroundColor('#ff4f31f7')
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .borderRadius(30)
      .onClick(() => {
        this.animate = !this.animate;
        // 第四步:閉包內通過狀態變量改變UI界面
        // 這裏可以寫任何能改變UI的邏輯比如數組添加,顯隱控制,系統會檢測改變後的UI界面與之前的UI界面的差異,對有差異的部分添加動畫
        // 組件一的rotate屬性發生變化,所以會給組件一添加rotate旋轉動畫
        this.rotateValue = this.animate ? 90 : 0;
        // 組件二的translate屬性發生變化,所以會給組件二添加translate偏移動畫
        this.translateX = this.animate ? 50 : 0;
        // 父組件column的opacity屬性有變化,會導致其子節點的透明度也變化,所以這裏會給column和其子節點的透明度屬性都加動畫
        this.opacityValue = this.animate ? 0.6 : 1;
      })

      // 組件二
      Column() {
      }
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .backgroundColor('#ff46bcf3')
      .borderRadius(30)
      .opacity(this.opacityValue)
      .translate({ x: this.translateX })
      .animation({ curve: curves.springMotion() })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
2.3 使用keyframeAnimateTo產生屬性動畫
keyframeAnimateTo(param: KeyframeAnimateParam, keyframes: Array<KeyframeState>): void

keyframeAnimateTo接口參數中,第一個參數KeyframeAnimateParam為關鍵幀動畫的整體參數(包括延時、播放次數、結束回調、期望幀率),第二個參數是一個數組,每一項表示一個關鍵幀內的動畫行為;每一段動畫可單獨控制動畫參數(包括時長、Curve等)。

在同一屬性存在多段動畫過程的場景,可通過在結束回調中再創建新動畫實現,但寫法更復雜,且每次創建新動畫需要耗時,會有銜接卡頓現象。此場景更適宜用關鍵幀動畫實現。

以下示例主要演示如何通過keyframeAnimateTo來設置關鍵幀動畫。

效果圖

HarmonyOS:屬性動畫_HarmonyOS_03

示例代碼

@Entry
@Component
struct KeyframeAnimateToDemo {
  // 第一步: 聲明相關狀態變量
  @State rotateValue: number = 0; // 組件一旋轉角度
  @State translateX: number = 0; // 組件二偏移量
  @State opacityValue: number = 1; // 組件二透明度
  // 第二步:將狀態變量設置到相關可動畫屬性接口
  build() {
    Row() {
      // 組件一
      Column() {
      }
      .rotate({ angle: this.rotateValue })
      .backgroundColor('#317AF7')
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .borderRadius(30)
      .onClick(() => {
        // 第三步:調用keyframeAnimateTo接口
        this.getUIContext()?.keyframeAnimateTo({
          iterations: 1
        }, [
          {
            // 第一段關鍵幀動畫時長為800ms,組件一順時針旋轉90度,組件二的透明度變從1變為0.6,組件二的translate從0位移到50
            duration: 800,
            event: () => {
              this.rotateValue = 90;
              this.opacityValue = 0.6;
              this.translateX = 50;
            }
          },
          {
            // 第二段關鍵幀動畫時長為500ms,組件一逆時針旋轉90度恢復至0度,組件二的透明度變從0.6變為1,組件二的translate從50位移到0
            duration: 500,
            event: () => {
              this.rotateValue = 0;
              this.opacityValue = 1;
              this.translateX = 0;
            }
          }
        ]);
      })
      // 組件二
      Column() {
      }
      .justifyContent(FlexAlign.Center)
      .width(100)
      .height(100)
      .backgroundColor('#D94838')
      .borderRadius(30)
      .opacity(this.opacityValue)
      .translate({ x: this.translateX })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

説明

  • 在對組件的位置大小的變化做動畫的時候,由於佈局屬性的改變會觸發測量佈局,性能開銷大。scale屬性的改變不會觸發測量佈局,性能開銷小。因此,在組件位置大小持續發生變化的場景,如跟手觸發組件大小變化的場景,推薦使用scale。
  • 屬性動畫應該作用於始終存在的組件,對於將要出現或者將要消失的組件的動畫應該使用轉場動畫
  • 儘量不要使用動畫結束回調。屬性動畫是對已經發生的狀態進行的動畫,不需要開發者去處理結束的邏輯。如果要使用結束回調,一定要正確處理連續操作的數據管理。

三、自定義屬性動畫

屬性動畫是可動畫屬性的參數值發生變化時,引起UI上產生的連續視覺效果。當參數值發生連續變化,且設置到可以引起UI發生變化的屬性接口上時,就可以實現屬性動畫。

ArkUI提供@AnimatableExtend裝飾器,用於自定義可動畫屬性接口。由於參數的數據類型必須具備一定程度的連續性,自定義可動畫屬性接口的參數類型僅支持number類型和實現AnimatableArithmetic\接口的自定義類型。通過自定義可動畫屬性接口和可動畫數據類型,在使用animateTo或animation執行動畫時,通過逐幀回調函數修改不可動畫屬性接口的值,能夠讓不可動畫屬性接口實現動畫效果。也可通過逐幀回調函數每幀修改可動畫屬性的值,實現逐幀佈局的效果。

3.1 使用number數據類型和@AnimatableExtend裝飾器改變Text組件寬度實現逐幀佈局的效果
// 第一步:使用@AnimatableExtend裝飾器,自定義可動畫屬性接口
@AnimatableExtend(Text)
function animatableWidth(width: number) {
  .width(width) // 調用系統屬性接口,逐幀回調函數每幀修改可動畫屬性的值,實現逐幀佈局的效果。
}

@Entry
@Component
struct AnimatablePropertyExample {
  @State textWidth: number = 80;

  build() {
    Column() {
      Text("AnimatableProperty")
        .animatableWidth(this.textWidth)// 第二步:將自定義可動畫屬性接口設置到組件上
        .animation({ duration: 2000, curve: Curve.Ease }) // 第三步:為自定義可動畫屬性接口綁定動畫
      Button("Play")
        .onClick(() => {
          this.textWidth = this.textWidth == 80 ? 160 : 80; // 第四步:改變自定義可動畫屬性的參數,產生動畫
        })
    }.width("100%")
    .padding(10)
  }
}