HarmonyOS Next 动效开发指导(二):添加转场动画(1)

2,159 阅读5分钟

1 转场动画定义

如前文所言,转场动画即是组件出现或消失时,对其添加动画的接口。

转场动画是指对将要出现或消失的组件做动画,对始终出现的组件做动画应使用属性动画。转场动画主要为了让开发者从繁重的消失节点管理中解放出来,如果用属性动画做组件转场,开发者需要在动画结束回调中删除组件节点。同时,由于动画结束前已经删除的组件节点可能会重新出现,还需要在结束回调中增加对节点状态的判断。

因此转场动画可以理解为是OpenHarmony动效框架为出现消失场景的动画做的系统侧支持。

具体而言,转场动画可以分为基础转场和高级模板化转场。

注:本文使用的接口为api 10的接口,请确保按照HarmonyOS应用开发环境搭建文档进行正确的环境搭建并且按照上述文档新建的是OpenHarmony项目,才能使用api 10的接口。

2 基础转场

基础转场的接口为.transition,用法为加在需要做出现消失动画的组件上,当组件出现消失时系统侧会对其添加用户定制的动画效果。基础转场支持以下效果:

transition.png

2.1 简易转场demo

例如,有一个按钮,需要点击后出现一张图片,基础的代码为:

@Entry
@Component
struct Index {
  @State isImageShow: boolean = false;

  build() {
    Column() {
      Button('Click Me')
        .width(100)
        .height(30)
        .margin(40)
        .onClick(() => {
          this.isImageShow = !this.isImageShow;
        })

      // 按钮点击会改变这个bool值,控制图片的出现消失
      if (this.isImageShow) {
        // 需要在reources/base/media目录下添加一张名为island的图片
        Image($r('app.media.island'))
          .size({ width: '80%', height: 300 })
      }
    }
    .size({ width: '100%', height: '100%' })
  }
}

不带转场.gif

现在要求图片出现时,是从下往上出现,则这种情况可以通过对图片添加转场动画实现:

Image($r('app.media.island'))
  .size({ width: '80%', height: 300 })
  // 添加转场效果,出现时从最终位置y轴50vp距离处开始进入,退出时退到y轴50vp距离处,且动画时长为500ms
  .transition(TransitionEffect.translate({ y: 50 }).animation({ duration: 500, curve: Curve.Friction }))

这里的transition接口传入的值表示组件会从y轴距终点位置距离50位置处开始,经过500ms,按照Friction曲线动到终点位置,由于y轴向下为正,所以整体是上移出现的效果,消失时是下移消失的效果:

位移转场.gif

为了更好的符合物理世界的规律,可以加上透明度的转场动画。添加的方式为在TransitionEffect后加.combine链式调用,可以添加多个想要的转场效果实现复杂的转场动效,这里以另外添加透明度转场为例:

Image($r('app.media.island'))
  .size({ width: '80%', height: 300 })
  // 添加转场效果,出现时从最终位置y轴50vp距离处开始进入,退出时退到y轴50vp距离处,且动画时长为500ms
  .transition(TransitionEffect.translate({ y: 50 }).animation({ duration: 500, curve: Curve.Friction })
    // 添加透明度转场,如果不指定animation,则动画跟随combine的上一个转场的animation,此处动画即为500ms的透明度动画
    .combine(TransitionEffect.OPACITY))

效果为:

位移+透明度.gif

上述实现是在transition中指定出现消失的动画参数的。在OpenHarmony中,如果控制组件出现消失的flag值是通过动画改变的,那么TransitionEffect也可以不指定.animation,这样会跟随flag的动画属进行动画。如下述代码,效果与上面的效果是一致的:

@Entry
@Component
struct Index {
  @State isImageShow: boolean = false;

  build() {
    Column() {
      Button('Click Me')
        .width(100)
        .height(30)
        .margin(40)
        .onClick(() => {
          // 此处对控制出现消失的isImageShow值加动画
          animateTo({
            duration: 500,
            curve: Curve.Friction
          }, () => {
            this.isImageShow = !this.isImageShow;
          });
        })

      // 按钮点击会改变这个bool值,控制图片的出现消失
      if (this.isImageShow) {
        // 需要在reources/base/media目录下添加一张名为island的图片
        Image($r('app.media.island'))
          .size({ width: '80%', height: 300 })
          // 添加转场效果,出现时从最终位置y轴50vp距离处开始进入,退出时退到y轴50vp距离处
          // 此时不指定animation,会跟随isImageShow的动画
          .transition(TransitionEffect.translate({ y: 50 })
            // 添加透明度转场,此时跟随的是isImageShow的动画
            .combine(TransitionEffect.OPACITY))
      }
    }
    .size({ width: '100%', height: '100%' })
  }
}

2.2 多个组件的出现消失添加时序demo

对于多个元素的转场,可以对各元素的animation参数中的delay设置不同的值,实现各组件依次出现的效果,可以参考官方文档的demo

const ITEM_COUNTS = 9;
const ITEM_COLOR = '#ED6F21';
const INTERVAL = 30;
const DURATION = 300;

@Entry
@Component
struct Index1 {
  @State isGridShow: boolean = false;

  private dataArray: number[] = new Array(ITEM_COUNTS);

  // 在页面的aboutToAppear生命周期里初始化列表
  aboutToAppear(): void {
    for (let i = 0; i < ITEM_COUNTS; i++) {
      this.dataArray[i] = i;
    }
  }

  build() {
    Stack() {
      if (this.isGridShow) {
        Grid() {
          // ForEach循环渲染各个小方格
          ForEach(this.dataArray, (item: number, index: number) => {
            GridItem() {
              Stack() {
                Text((item + 1).toString())
              }
              .size({ width: 50, height: 50 })
              .backgroundColor(ITEM_COLOR)
              .transition(TransitionEffect.OPACITY
                .combine(TransitionEffect.scale({ x: 0.5, y: 0.5 }))
                // 对每个方格的转场添加delay,实现组件的渐次出现消失效果,这里设置delay的间隔为30ms
                .animation({ duration: DURATION, curve: Curve.Friction, delay: INTERVAL * index }))
              .borderRadius(10)
            }
            // 消失时,如果不对方格的所有父控件添加转场效果,则方格的消失转场不会生效
            // 此处让方格的父控件在出现消失转场时一直以0.99的透明度显示,使得方格的转场效果不受影响
            .transition(TransitionEffect.opacity(0.99))
          }, (item: number) => item.toString())
        }
        .columnsTemplate('1fr 1fr 1fr')
        .rowsGap(15)
        .columnsGap(15)
        .size({ width: 180, height: 180 })
        // 消失时,如果不对方格的所有父控件添加转场效果,则方格的消失转场不会生效
        // 此处让父控件在出现消失转场时一直以0.99的透明度显示,使得方格的转场效果不受影响
        .transition(TransitionEffect.opacity(0.99))
      }
    }
    .size({ width: '100%', height: '100%' })
    .onClick(() => {
      // 控制逻辑,点击页面后,通过动画控制grid的显隐
      animateTo({
        duration: DURATION + INTERVAL * (ITEM_COUNTS - 1),
        curve: Curve.Friction
      }, () => {
        this.isGridShow = !this.isGridShow;
      })
    })
  }
}

由此实现的效果为:

multi-transition-delay.gif

2.3 翻转出现的demo

在现实生活中,有很多物体翻转的场景,在应用开发过程中,也会有很多效果借鉴这些场景,如响应下拉事件出现的界面,里面的组件就可以使用这种动效。现在就以转场动画实现这些效果。

实现方式为对组件添加一个向下位移的转场,同时combine缩放和透明度以及x轴旋转的转场动效。这里结合上面的渐次出现的效果,写一个相关的demo:

const ITEM_COUNTS = 5;
const INTERVAL = 50;
const DURATION = 400;

@Entry
@Component
struct Index1 {
  @State isComponentsShow: boolean = false;

  private colorData: string[] = ['#46B1E3', '#61CFBE', '#A5D61D', '#ED6F21', '#F7CE00'];

  build() {
    Column({ space: 30 }) {
      if (this.isComponentsShow) {
        ForEach(this.colorData, (item: string, index: number) => {
          Column()
            .size({ width: '80%', height: 100 })
            .backgroundColor(item)
            .borderRadius(20)
            // 此处添加透明度+缩放+旋转+位移转场效果并添加delay实现依次出现的效果
            .transition(TransitionEffect.OPACITY
              .combine(TransitionEffect.scale({ x: 0.6, y: 0.6 }))
              .combine(TransitionEffect.rotate({ x: 1, angle: 60 }))
              .combine(TransitionEffect.translate({ y: -100 }))
              .animation({ duration: DURATION, curve: Curve.Friction, delay: 160 - INTERVAL * index }))
        }, (item: string) => item)
      }
    }
    .padding({ top: 80 })
    .size({ width: '100%', height: '100%' })
    .onClick(() => {
      animateTo({
        duration: DURATION + (ITEM_COUNTS - 1) * INTERVAL,
        curve: Curve.Sharp
      }, () => {
        this.isComponentsShow = !this.isComponentsShow;
      })
    })
  }
}

效果为:

delay-window.gif