动画animation 指南

发布时间 2023-08-02 14:36:44作者: cps666

属性动画

属性动画:组件的某些通用属性变化时,可以通过属性动画实现渐变效果,提升用户体验。
支持的属性包括width、height、backgroundColor、opacity、scale、rotate、translate等。
属性动画设置主要涉及duration(动画时长)、tempo(动画速率)、delay(动画延时)、curve(动画曲线)、palyMode(动画模式)和iterations(动画播放次数)。

属性动画,是最为基础的动画,其功能强大、使用场景多,应用范围较广。常用于如下场景中:

一、页面布局发生变化。例如添加、删除部分组件元素。
二、页面元素的可见性和位置发生变化。例如显示或者隐藏部分元素,或者将部分元素从一端移动到另外一端。
三、页面中图形图片元素动起来。例如使页面中的静态图片动起来。

简单来说,属性动画是组件的通用属性发生改变时而产生的属性渐变效果。如下图所示,其原理是,当组件的通用属性发生改变时,组件状态由初始状态逐渐变为结束状态的过程中,会创建多个连续的中间状态,逐帧播放后,就会形成属性渐变效果,从而形成动画。

属性动画的使用方式也非常简单,只需要给组件(包括基础组件和容器组件)添加animation属性,并设置好参数,如下代码所示:

Image($r('app.media.image1'))   
   .animation({   
      duration: 1000,    
      tempo: 1.0,    
      delay: 0,    
      curve: Curve.Linear,    
      playMode: PlayMode.Normal,    
      iterations: 1  
   })
   
1、animation属性作用域。animation自身也是组件的一个属性,其作用域为animation之前。即产生属性动画的属性须在animation之前声明,其后声明的将不会产生属性动画。
2、产生属性动画的属性变化时需触发UI状态更新。
3、产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画。目前支持的属性包括width、height、position、opacity、backgroundColor、scale、rotate、translate等

给应用添加动画

属性动画,是最为基础的动画,其功能强大、使用场景多,应用范围较广。常用于如下场景中:

  • 一、页面布局发生变化。例如添加、删除部分组件元素。
  • 二、页面元素的可见性和位置发生变化。例如显示或者隐藏部分元素,或者将部分元素从一端移动到另外一端。
  • 三、页面中图形图片元素动起来。例如使页面中的静态图片动起来。

简单来说,属性动画是组件的通用属性发生改变时而产生的属性渐变效果。如下图所示,其原理是,当组件的通用属性发生改变时,组件状态由初始状态逐渐变为结束状态的过程中,会创建多个连续的中间状态,逐帧播放后,就会形成属性渐变效果,从而形成动画。

属性动画的使用方式也非常简单,只需要给组件(包括基础组件和容器组件)添加 animation 属性,并设置好参数,如下代码所示:

Image($r('app.media.image1'))   
   .animation({   
      duration: 1000,    
      tempo: 1.0,    
      delay: 0,    
      curve: Curve.Linear,    
      playMode: PlayMode.Normal,    
      iterations: 1  
   })

如下图所示,在该下拉刷新动画场景中,一共有 6 个属性动画。头部中的五个图标的移动放大动画中,每个图标都是单独的一个动画,其共同组合成一个刷新等待动画。最后是下方组件上移的一个移动动画。为方便理解,图中下方的内容将以图片来代替实际应用的功能页面。

图 2-1 :示例动画

该 6 个属性动画创建方式类似,以五个图标放大移动动画的为例来讲解如何创建属性动画。

首先,创建一个头部刷新组件 RefreshAnimHeader,在其中自定义一个图标组件 AttrAnimIcons,用 Image 组件将资源图标引入,并设置好样式,如下所示:

@Component
export default struct RefreshAnimHeader {
  ...
  @Builder AttrAnimIcons(iconItem) {  
    Image(iconItem.imgRes)    
      .width(this.iconWidth)    
      .position({ x: iconItem.posX })    
      .objectFit(ImageFit.Contain)    
      .animation({      
        duration: 2000,      
        tempo: 3.0,      
        delay: iconItem.delay,      
        curve: Curve.Linear,      
        playMode: PlayMode.Alternate,      
        iterations: -1
     })
  }
  ...
}

然后在 build 方法中使用 Row 容器组件,将自定义的图标组件引入,并设置好样式,同时定义组件状态 iconWidth,添加 onApper 事件,修改 iconWidth 的值,使其从 30 变为 100,触发 UI 状态更新。

@Component
export default struct RefreshAnimHeader {
  ...
  @State iconWidth: number = 30;
  private onStateCheck() {  
    if (this.state === RefreshState.REFRESHING) {    
      this.iconWidth = 100;  
    } else {    
      this.iconWidth = 30;  
    }
  }
  build() {  
    Row() {    
      ForEach(CommonConstants.REFRESH_HEADER_FEATURE, (iconItem) => {     
        this.AttrAnimIcons(iconItem)    
      }, item => item.toString())  
    }  
     .width("100%")  
     .height("100%")  
     .onAppear(() => {    
       this.onStateCheck();
     })
  }
}

运行代码,即可看到五个图标的移动放大动画效果。

1、animation 属性作用域。animation 自身也是组件的一个属性,其作用域为 animation 之前。即产生属性动画的属性须在 animation 之前声明,其后声明的将不会产生属性动画。以示例中的五个图标动画为例,我们期望产生动画的属性为 Image 组件的 width 属性,故该属性 width 需在 animation 属性之前声明。如果将该属性 width 在 animation 之后声明,则不会产生动画效果。

2、产生属性动画的属性变化时需触发 UI 状态更新。在本示例中,产生动画的属性 width,其值是通过变量 iconWidth 从 30 变为 100,故该变量 iconWidth 的改变需触发 UI 状态更新。

3、产生属性动画的属性本身需满足一定的要求,并非任何属性都可以产生属性动画。目前支持的属性包括 width、height、position、opacity、backgroundColor、scale、rotate、translate 等

属性动画中 animation 的参数如下:

属性名称

属性类型

默认值

描述

duration

number

1000

动画时长,单位为毫秒,默认时长为 1000 毫秒。

tempo

number

1.0

动画的播放速度,值越大动画播放越快,值越小播放越慢,为 0 时无动画效果。

curve

Curve

Curve.Linear

动画变化曲线,默认曲线为线性。

delay

number

0

延时播放时间,单位为毫秒,默认不延时播放。

iterations

number

1

播放次数,默认一次,设置为 - 1 时表示无限次播放。

playMode

PlayMode

PlayMode.Normal

设置动画播放模式,默认播放完成后重头开始播放。

onFinish

function

-

动画播放结束时回调该函数。

其中变化曲线 curve 枚举值为:

名称

描述

Linear

表示动画从头到尾的速度都是相同的。

Ease

表示动画以低速开始,然后加快,在结束前变慢,CubicBezier(0.25, 0.1, 0.25, 1.0)。

EaseIn

表示动画以低速开始,CubicBezier(0.42, 0.0, 1.0, 1.0)。

EaseOut

表示动画以低速结束,CubicBezier(0.0, 0.0, 0.58, 1.0)。

EaseInOut

表示动画以低速开始和结束,CubicBezier(0.42, 0.0, 0.58, 1.0)。

FastOutSlowIn

标准曲线,cubic-bezier(0.4, 0.0, 0.2, 1.0)。

LinearOutSlowIn

减速曲线,cubic-bezier(0.0, 0.0, 0.2, 1.0)。

FastOutLinearIn

加速曲线,cubic-bezier(0.4, 0.0, 1.0, 1.0)。

ExtremeDeceleration

急缓曲线,cubic-bezier(0.0, 0.0, 0.0, 1.0)。

Sharp

锐利曲线,cubic-bezier(0.33, 0.0, 0.67, 1.0)。

Rhythm

节奏曲线,cubic-bezier(0.7, 0.0, 0.2, 1.0)。

Smooth

平滑曲线,cubic-bezier(0.4, 0.0, 0.4, 1.0)。

Friction

阻尼曲线,CubicBezier(0.2, 0.0, 0.2, 1.0)。

播放模式 playMode 枚举值为:

名称

描述

Normal

动画按正常播放。

Reverse

动画反向播放。

Alternate

动画在奇数次(1、3、5...)正向播放,在偶数次(2、4、6...)反向播放。

AlternateReverse

动画在奇数次(1、3、5...)反向播放,在偶数次(2、4、6...)正向播放。

本文以参数 delay 和 onFinish 为例来演示和讲解属性动画的参数调整。其他参数的效果可自行尝试。

延时播放时间 delay 的设置

在单个的组件元素的属性动画中,一般不设置参数 delay 的值。而在需要设置时,往往是需要在动画开始前做一些准备工作,具体依场景而定,本文在此不讨论。

在由多个组件元素的属性动画组合的动画中,例如示例动画中的五个图标的属性动画组合而成的刷新等待动画,通过设置参数 delay 的值,可以使各个组件元素之间按照一定的秩序依次播放,形成跌宕起伏、鳞次栉比的动画效果。在此场景中,该值的大小又与 duration 相关联。

该如何设置各个图标的参数 delay 的值呢?

在设置 delay 值之前,我们先理解一个概念:延时间距。其意思是两个图标组件的延时参数 delay 的差值,即:delay2-delay1 = 延时间距。要想实现五个图标之间以良好的秩序先后移动放大,各个图标之间的延时间距是一样的,例如延时间距为 100ms 时,此五个图标的延时 delay 可以分别设置为 100ms、200ms、300ms、400ms、500ms。

在实际开发场景中,我们该如何确定延时间距呢?

在此有个经验可以参考:在延时间距不超过动画时长 duration 时,总延时间距越接近 duration,秩序性越好。其中,总延时间距为延时间距与图标数量的乘积,即:延时间距 * 图标数量 = 总延时间距。

故此,我们在设置参数 delay 时,需要确定动画时长 duration 的值。该值默认为 1000ms,具体时长可依据具体的业务需要来决定。

在本示例动画中,图标动画时长 duration 为 2000ms,故延时间距为 2000ms/5=400ms,五个图标的延时参数 delay 可分别设置为 400ms、800ms、1200ms、1600ms、2000ms。其效果如示例图所示,图标先后秩序明显,视觉效果良好。

onFinish 回调函数的使用

参数 onFinish 与参数 iterations 有关。当参数 iterations 播放结束时,会调用 onFinish 函数来进行后续的业务处理。例如提示动画播放结束。

Image(iconItem.imgRes)
  .width(this.iconWidth)
  .position({ x: iconItem.posX })
  .objectFit(ImageFit.Contain)
  .animation({
    duration: 2000,
    tempo: 3.0,
    delay: iconItem.delay,
    curve: Curve.Linear,
    playMode: PlayMode.Normal,
    iterations: 1,
    onFinish: () => {      
      prompt.showToast({ message:"动画播放结束!!!" })
    }
  })

当 iterations 设置为 - 1 时,表示无限次播放,则 onFinish 回调函数不会被调用。

此处需要将关闭属性动画区别开来:

  • 属性动画关闭,是指动画播放结束,但是动画组件依然存在并显示在页面上。
  • 关闭属性动画页面,是指将动画的组件删除或者隐藏起来。

在本示例动画中,指将头部刷新组件 RefreshAnimHeader 隐藏起来。该如何实现呢?

首先,在组件 RefreshAnimHeader 中添加变量 state,并用 @Consume 监听其值的变化,同时添加条件渲染逻辑,根据 state 的值来判断是否需要关闭。当 state 变为 IDLE 状态时,表示需要关闭属性动画页面。

@Component
export default struct RefreshAnimHeader {  
  @Consume(RefreshConstants.REFRESH_STATE_TAG) @Watch('onStateCheck') state: RefreshState;
  build() {
    Row() {
      if (this.state !== RefreshState.IDLE) { // start or stop animation when idle state.
        ForEach(CommonConstants.REFRESH_HEADER_FEATURE, (iconItem) => {
          this.AttrAnimIcons(iconItem)
        }, item => item.toString()}
      }
    }
    .width(CommonConstants.FULL_LENGTH)
    .height(CommonConstants.FULL_LENGTH)
    .onAppear(() => {
      this.onStateCheck();
    })
  }
}

其次,在本示例中,通过下方图片的上移属性动画来关闭刷新组件 RefreshAnimHeader。在组件 RefreshComponent 中,通过 @Consume 与组件 RefreshAnimHeader 的 @Consume 进行间接绑定,修改 state 变量的值为 IDLE 状态即可关闭属性动画页面。

@Component
export default struct RefreshComponent {
  @Consume(RefreshConstants.REFRESH_STATE_TAG) @Watch('onStateChanged') state: RefreshState;
  build() {
    List({ scroller: this.listController }) {
      ListItem() {
         ...
      }
  }  
  .animation({
    curve: Curve.Smooth,
    duration: RefreshConstants.REFRESH_HEADER_ANIM_DURATION,
    playMode: PlayMode.Normal,
    onFinish: () => {
      if (this.headerOffset === -RefreshConstants.REFRESH_HEADER_HEIGHT) {
        this.state = RefreshState.IDLE;
      }
    }
  })
}

具体代码信息请参考 Codelab:自定义下拉刷新动画(ArkTS)