HarmonyOS应用开发探索之旅:探索未来的技术前沿-06-炫酷的转场动画

系列文章目录

HarmonyOS应用开发之道:掌握未来,领略技术的魅力-01-ArkTS基础知识

HarmonyOS应用开发全攻略:从入门到精通-02-程序框架UIAbility、启动模式与路由跳转

HarmonyOS应用开发系列:探索未来的技术前沿-03-基础组件-让我们来码出复杂酷炫的UI

HarmonyOS应用开发指南:为未来做好准备-04-组件状态管理

HarmonyOS应用开发必修课-05-让我们的界面动起来!- 显示动画与属性动画

HarmonyOS应用开发探索之旅:探索未来的技术前沿-06-炫酷的转场动画


前言

本节记录学习下HamronyOS中的动画,运用到组件动画、转场动画等场景,让我们的UI界面炫酷起来~

先上效果图:

共享元素转场动画 系统弹窗 组件内转场动画1 组件内转场动画2
组件内转场动画3 页面间转场动画1 页面间转场动画2 页面间转场动画3
--- --- --- ---

回顾下动画的分类:

按页面分类:

按基础能力分类

接下来分析介绍页面转场动画

  • 页面间转场:页面转场通过在全局pageTransition方法内配置页面入场组件和页面退场组件来自定义页面转场动效。
  • 组件内转场:组件转场主要通过transition属性进行配置转场参数,在组件插入和删除时进行过渡动效,主要用于容器组件子组件插入删除时提升用户体验(需要配合animateTo才能生效,动效时长、曲线、延时跟随animateTo中的配置)。
  • 共享元素转场:通过修改共享元素的sharedTransition属性设置元素在不同页面之间过渡动效。例如,如果两个页面使用相同的图片(但位置和大小不同),图片就会在这两个页面之间流畅地平移和缩放。

一、组内转场动画

1、组内转场动画

组件的插入、删除 过程即为组件本身的转场过程,组件的插入、删除动画称为组件内转场动画。组件内转场动画主要用于容器组件在子组件添加和删除时添加的动画效果,通过组件内转场动画,可定义组件出现、消失的效果。目的在于提升用户体验,通过transition 属性方法配置动画参数

注意 : 组件内转场动画需要配合animateTo才能生效,动效时长、曲线、延时跟随animateTo中的配置。

📢:组件内的转场动画时长,动画曲线等动画参数以animationTo方法设置的为基准。

typescript 复制代码
declare class CommonTransition<T> {
  transition(value: TransitionOptions): T;
}

declare interface TransitionOptions {
  type?: TransitionType;
  opacity?: number;
  translate?: TranslateOptions;
  scale?: ScaleOptions;
  rotate?: RotateOptions;
}
  • value :用于设置组件转场的动画效果,TransitionOptions

    • type :设置组件添加和删除时的动画效果,TransitionType

      • All(默认值):设置组件添加和删除时的动画效果。
      • Insert:设置组件添加时的动画效果。
      • Delete:设置组件删除时的动画效果。
    • opacity:设置组件转场时的透明度效果,为插入时起点和删除时终点的值。

    • translate :设置组件转场时的平移效果,为插入时起点和删除时终点的值。TranslateOptions

      • x:设置组件在 X 轴上的位移。
      • y:设置组件在 Y 轴上的位移。
      • z:设置组件在 Z 轴上的位移。
    • scale :设置组件转场时的缩放效果,为插入时起点和删除时终点的值。ScaleOptions

      • x:设置组件在 X 轴上的缩放。
      • y:设置组件在 Y 轴上的缩放。
      • z:设置组件在 Z 轴上的缩放。
      • centerX:设置组件缩放的中心点 X 坐标。
      • centerY:设置组件缩放的中心点 Y 坐标。
    • rotate :设置组件转场时的旋转效果,为插入时起点和删除时终点的值。RotateOptions

      • x:设置组件在 X 轴上的旋转。
      • y:设置组件在 Y 轴上的旋转。
      • z:设置组件在 Z 轴上的旋转。
      • centerX:设置组件旋转的中心点 X 坐标。
      • centerY:设置组件旋转的中心点 Y 坐标。

卡片展开收缩动画示例:

效果图:

代码实现:

typescript 复制代码
import router from '@ohos.router';
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';
import prompt from '@system.prompt';
// import DataItemBean from '../viewmodel/DataItemBean';

@Component
export default struct StudentListItem {

  ...
 
  build() {
    if (this.isListModel) {
      Column() {

        ...

        if (this.isChecked) {
          Row() {
            Text(this.studentData.title + " is very handsome!")
              .fontSize('18fp')
              .fontColor(Color.Blue)

            Blank()

            Image(this.studentData.image)
              .objectFit(ImageFit.Cover)
              .width('50vp')
              .height('50vp')
              .borderRadius(100)
              .onClick(this.onItemChildImageClick)
          }
          .width('100%')
          .padding({ left: 10, right: 10 })
          .justifyContent(FlexAlign.SpaceAround)
          // 组内转场动画
          .transition({ // 设置Image转场动画
            type: TransitionType.Insert, // 设置Image的入场动画
            scale: CommonConstants.COMPONENT_TRANSITION_SCALE_OPEN,
            opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
          })
          .transition({ // 设置Image转场动画
            type: TransitionType.Delete, // 设置Image的退场动画
            scale: CommonConstants.COMPONENT_TRANSITION_SCALE_CLOSE,
            opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
          })
        }
      }
      .borderRadius(22)
      .backgroundColor($r('app.color.start_window_background'))
      .width('100%')
      .height(this.isChecked ? $r('app.float.list_item_expand_height') : $r('app.float.list_item_height'))
      // 属性动画
      // animation属性作用域:animation自身也是组件的一个属性,其作用域为animation之前。
      // 即产生属性动画的属性须在animation之前声明,其后声明的将不会产生属性动画
      // 显式动画把要执行动画的属性的修改放在闭包函数中触发动画,而属性动画则无需使用闭包,把animation属性加在要做属性动画的组件的属性后即可
      .animation({ duration: 600 })
      .onClick(() => {
        // 组件内转场动画需要配合 animateTo 才能生效,动效时长、曲线、延时跟随 animateTo 中的配置
        // 📢:组件内的转场动画时长,动画曲线等动画参数以 animationTo 方法设置的为基准
        // 显示动画-添加展开动画
        animateTo({ duration: 1000 }, () => {
          this.isChecked = !this.isChecked;
        })

        // 收起非点击Item
        this.clickIndex = this.index;

      })
    } else {

      ...

    }
  }
}
typescript 复制代码
/**
 * Common constants for all features.
 */
export default class CommonConstants {

  /**
   * Component transition opacity.
   */
  static readonly COMPONENT_TRANSITION_OPACITY = 0;

  static readonly COMPONENT_NO_TRANSITION_OPACITY = 1;

  /**
   * Component transition scale.
   */
  static readonly COMPONENT_TRANSITION_SCALE_OPEN: CustomTransition = {
    x: 0.5, y: 0.5
  };

  static readonly COMPONENT_TRANSITION_SCALE_CLOSE: CustomTransition = {
    x: 0.0, y: 0.0
  };

  /**
   * Transition animation duration.
   */
  static readonly TRANSITION_ANIMATION_DURATION: number = 600;

}
核心代码块:
typescript 复制代码
          Row() {
           ...
          }
          // 组内转场动画
          .transition({ // 设置Image转场动画
            type: TransitionType.Insert, // 设置Image的入场动画
            scale: CommonConstants.COMPONENT_TRANSITION_SCALE_OPEN,
            opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
          })
          .transition({ // 设置Image转场动画
            type: TransitionType.Delete, // 设置Image的退场动画
            scale: CommonConstants.COMPONENT_TRANSITION_SCALE_CLOSE,
            opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
          })
          
      ...
      
    // 组件内转场动画需要配合 animateTo 才能生效,动效时长、曲线、延时跟随 animateTo 中的配置
    // 📢:组件内的转场动画时长,动画曲线等动画参数以 animationTo 方法设置的为基准
    // 显示动画-添加展开动画
    animateTo({ duration: 1000 }, () => {
      this.isChecked = !this.isChecked;
    })
实现效果:

实现效果就是当点击Item卡片的时候,动画展开图片卡片,且Image的入场动画是从中间向两边展开,开始时透明度为0:static readonly COMPONENT_TRANSITION_SCALE_OPEN: CustomTransition = { x: 0.5, y: 0.5 }; 再次点击Item的时候,收缩图片卡片,且Image的退场动画是从两边向中心收回,透明度从1到0慢慢渐变:static readonly COMPONENT_TRANSITION_SCALE_CLOSE: CustomTransition = { x: 0.0, y: 0.0 };

2、组内转场动画-结合系统弹框

系统类弹窗很简单,用到的时候在做代码讲解,可以看下官网介绍,附链接:

警告弹窗

列表选择弹窗

自定义弹窗

日期滑动选择弹窗

时间滑动选择弹窗

文本滑动选择弹窗

这里介绍下自定义弹窗的使用方法:
自定义弹窗的使用更加灵活,适用于更多的业务场景,在自定义弹窗中您可以自定义弹窗内容,构建更加丰富的弹窗界面。自定义弹窗的界面可以通过装饰器@CustomDialog定义的组件来实现,然后结合CustomDialogController来控制自定义弹窗的显示和隐藏。

typescript 复制代码
declare class CustomDialogController {
  constructor(value: CustomDialogControllerOptions); // 对话框控制器,控制弹框样式等
  open();                                            // 打开对话框
  close();                                           // 关闭对话框
}

// 配置参数的定义
declare interface CustomDialogControllerOptions {
  builder: any;                                      // 弹框构造器
  cancel?: () => void;                               // 点击蒙层的事件回调
  autoCancel?: boolean;                              // 点击蒙层是否自动消失
  alignment?: DialogAlignment;                       // 弹框在竖直方向上的对齐方式
  offset?: Offset;                                   // 根据alignment的偏移
  customStyle?: boolean;                             // 是否是自定义样式
  gridCount?: number;                                // grid数量
}

CustomDialogController 定义了 open()close() 方法:

  • open:打开对话框,如果对话框已经打开,则再次打开无效。

  • close:关闭对话框,如果对话框已经关闭,则再次关闭无效。

  • value :创建控制器需要的配置参数, CustomDialogControllerOptions 说明如下:

    • builder:创建自定义弹窗的构造器。
    • cancel:点击蒙层的事件回调。
    • autoCancel:是否允许点击遮障层退出。
    • alignment:弹窗在竖直方向上的对齐方式。
    • offset :弹窗相对 alignment 所在位置的偏移量。
    • customStyle:弹窗容器样式是否自定义。

自定义对话框也是自定义UI组件,只是自定义弹窗使用的修饰符为 @CustomDialog ,其它方面和自定义组件的流程是一致的.

自定义弹窗布局:StudentCardDialog.ets
typescript 复制代码
import { DataItemBean } from '../viewmodel/DataItemBean';

@CustomDialog
export struct StudentCardDialog {
  private studentData: DataItemBean;
  controller: CustomDialogController
  // 若尝试在CustomDialog中传入多个其他的Controller,以实现在CustomDialog中打开另一个或另一些CustomDialog,那么此处需要将指向自己的controller放在最后
  // cancel: () => void
  // confirm: () => void

  build() {
    // dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
    RelativeContainer() {
      Image(this.studentData.image)
        .objectFit(ImageFit.Cover)
        .width('300vp')
        .height('450vp')
        .borderRadius(24)
        .alignRules({
          top: { anchor: "__container__", align: VerticalAlign.Top },
          left: { anchor: "__container__", align: HorizontalAlign.Start }
        })
        .id("image")

      Text(this.studentData.title)
        .fontSize('18fp')
        .fontColor(Color.White)
        .backgroundColor($r('app.color.transparent_backgroundColor'))
          // .height('16vp')
        .borderRadius({ topLeft: 24, bottomRight: 24 })
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .alignRules({
          bottom: { anchor: "image", align: VerticalAlign.Bottom },
          right: { anchor: "image", align: HorizontalAlign.End }
        })
        .id("text")
    }
    .width('300vp')
    .height('450vp')
    .onClick(() => {
      this.controller.close()
    })
  }
}
StudentListPage.ets 中弹出自定义Dialog:

通过CustomDialogController类显示自定义弹窗。使用弹窗组件时,可优先考虑自定义弹窗,便于自定义弹窗的样式与内容。

typescript 复制代码
import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';
import { StudentCardDialog } from '../view/StudentCardDialog';
import { PreferencesUtil } from '../common/db/PreferencesUtil';
import Logger from '../common/utils/Logger';
import { CustomStudentCardDialog } from '../view/CustomStudentCardDialog';
// import DataItemBean from '../viewmodel/DataItemBean';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {
  
  ...

  // 弹窗控制器
  private dialogController: CustomDialogController = new CustomDialogController({
    builder: StudentCardDialog({
      // cancel: () => {
      //   this.showToast('Cancel')
      // },
      // confirm: () => {
      //   this.showToast(this.confirmMessage)
      // },
      // studentData: this.studentList2[0],
      studentData: this.currentClickItemData,
    }),
    autoCancel: true,
    alignment: DialogAlignment.Center,
    customStyle: true,
  })

  ...

  aboutToDisappear() {
    // 在自定义组件即将析构销毁时将dialogControlle删除和置空
    delete this.dialogController // 删除dialogController
    this.dialogController = undefined // 将dialogController置空
  }

  ...


  build() {
    Navigation() {
      Stack() {
        Row() {
          if (this.isListModel) {
            Scroll() {
              Column() {
                // Swiper组件
                this.SwiperBuilder(this.studentList2)
                // 列表
                List({ space: 16 }) {
                  ForEach(this.studentList2, (item: DataItemBean, index: number) => {
                    ListItem() {
                      // StudentListItem({ studentData: item, isListModel: true })

                      // 将父组件的列表显示模式状态this.isListModel传递给子组件的编辑模式状态isListModel
                      // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化
                      StudentListItem({
                        studentData: item,
                        index: index,
                        isListModel: this.isListModel,
                        clickIndex: $clickIndex, // 带有"@Link"装饰的属性必须初始化为"$"
                        onItemChildImageClick: () => {
                          this.currentClickItemData = item


                          if (this.dialogController != undefined) {
                              // 打开系统自定义弹框
                              this.dialogController.open()
                              this.showToast(item.title)
                          }
                        }
                      })
                    }
                  }, (item, index) => JSON.stringify(item) + index)
                }
                .divider({ strokeWidth: 1, color: Color.Gray, startMargin: 30, endMargin: 0 })

                Text('---没有更多了---').fontSize('16vp').margin('30vp')
              }
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
          } else {
            Scroll() {
              Column() {
                // Swiper组件
                this.SwiperBuilder(this.studentList2)

                Grid() {
                  ForEach(this.studentList2, (item: DataItemBean) => {
                    GridItem() {
                      // StudentListItem({ studentData: item, isListModel: false })
                      // 将父组件的列表显示模式状态this.isListModel传递给子组件的编辑模式状态isListModel
                      // 此处指定的参数都将在初始渲染时覆盖本地定义的默认值,并不是所有的参数都需要从父组件初始化
                      StudentListItem({
                        studentData: item,
                        isListModel: this.isListModel,
                        clickIndex: $clickIndex, // 带有"@Link"装饰的属性必须初始化为"$"
                        onItemChildImageClick: () => {
                          this.currentClickItemData = item

                          // 使用系统自定义弹框组件
                          if (this.dialogController != undefined) {
                            this.dialogController.open()
                            this.showToast(item.title)
                          }
                        }
                      })
                    }
                  }, (item: string) => JSON.stringify(item))
                }
                .columnsTemplate('1fr 1fr 1fr')
                .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
                .columnsGap('10vp')
                .rowsGap('10vp')
                .height('640vp')
                // .layoutDirection(GridDirection.Row)

                Text('---没有更多了---').fontSize('16vp').margin('30vp')
              }
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
          }
        }
        .width('90%')
      }

    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}

效果很好,但是我们会发现最新的API中没有动画接口,那我们想要实现Dialog的入场与退场动画怎么实现呢?

  • 方法1:可以结合之前学习过的属性动画 + 显示动画 来实现,但是会有一些问题。
  • 方法2:在第3点我们结合自定义布局 + 组内转场动画 来实现这种效果。

先看下方法1效果:

代码:
typescript 复制代码
import { DataItemBean } from '../viewmodel/DataItemBean';

@CustomDialog
export struct StudentCardDialog {
  private studentData: DataItemBean;
  controller: CustomDialogController
  // 若尝试在CustomDialog中传入多个其他的Controller,以实现在CustomDialog中打开另一个或另一些CustomDialog,那么此处需要将指向自己的controller放在最后

  @State private opacityValue: number = 0;
  @State private angleValue: number = 0;
  @State private scaleValue: number = 0;

  build() {
    // dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
    RelativeContainer() {
      Image(this.studentData.image)
        .objectFit(ImageFit.Cover)
        .width('300vp')
        .height('450vp')
        .borderRadius(24)
        .alignRules({
          top: { anchor: "__container__", align: VerticalAlign.Top },
          left: { anchor: "__container__", align: HorizontalAlign.Start }
        })
        .id("image")

      Text(this.studentData.title)
        .fontSize('18fp')
        .fontColor(Color.White)
        .backgroundColor($r('app.color.transparent_backgroundColor'))
        .borderRadius({ topLeft: 24, bottomRight: 24 })
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .alignRules({
          bottom: { anchor: "image", align: VerticalAlign.Bottom },
          right: { anchor: "image", align: HorizontalAlign.End }
        })
        .id("text")
    }
    .width('300vp')
    .height('450vp')
    .onClick(() => {
      this.onCloseDialog();
    })
    // 使用属性动画实现系统自定义Dialog的显示隐藏动画效果
    .scale({ x: this.scaleValue, y: this.scaleValue })
    .rotate({ x: 0, y: 1, z: 0, angle: this.angleValue })
    .opacity(this.opacityValue)
    // 在obAppear()生命周期时,结合显示动画改变属性状态值
    .onAppear(() => {
      animateTo({
        duration: 800,
        curve: Curve.EaseOut,
        delay: 100,
        iterations: 1
      }, () => {
        this.opacityValue = 1;
        this.scaleValue = 1;
        this.angleValue = 360;
      });
    })
  }

  // 关闭弹框时的动画效果
  onCloseDialog(){
    animateTo({
      duration: 800,
      curve: Curve.EaseIn,
      delay: 100,
      iterations: 1
    }, () => {
      this.opacityValue = 0;
      this.scaleValue = 0;
      this.angleValue = 360;
    });

    // 使用相同时间段的延迟时间后,设置关闭弹窗
    // 延迟800毫秒退出Dialog
    setTimeout(() => {
      this.controller.close();
    }, 800)
  }
}

下面接着介绍方法2:在第3点我们结合自定义布局 + 组内转场动画 来实现这种效果。

3、组内转场动画-结合自定义布局

想要实现的效果是点击卡片布局中的头像Image组件,弹出图片Dialog,但是要加上弹出动画和隐藏动画,上面说到了,目前Dialog中没有提供动画过渡效果的接口,所以要实现弹框show和dismiss的动画效果,就需要我们自定义布局结合组内转场动画来实现了。

上面的动画效果是,点击弹出Dialog的时候,show的过程是透明度逐渐从不透明渐变为透明状态,且从中心点逐渐放大的过程,dismiss的过程正好是对称相反的。

代码实现:
  • 自定义布局 CustomStudentCardDialog.ets
typescript 复制代码
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';

@Component
export struct CustomStudentCardDialog {
  private studentData: DataItemBean;
  onDialogClick: () => void

  build() {
    // dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
    Row() {
      RelativeContainer() {
        Image(this.studentData.image)
          .objectFit(ImageFit.Cover)
          .width('300vp')
          .height('450vp')
          .borderRadius(24)
          .alignRules({
            top: { anchor: "__container__", align: VerticalAlign.Top },
            left: { anchor: "__container__", align: HorizontalAlign.Start }
          })
          .id("image")

        Text(this.studentData.title)
          .fontSize('18fp')
          .fontColor(Color.White)
          .backgroundColor($r('app.color.transparent_backgroundColor'))
            // .height('16vp')
          .borderRadius({ topLeft: 24, bottomRight: 24 })
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .alignRules({
            bottom: { anchor: "image", align: VerticalAlign.Bottom },
            right: { anchor: "image", align: HorizontalAlign.End }
          })
          .id("text")
      }
      .width('300vp')
      .height('450vp')
    }
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .height('100%')
    // 组内转场动画
    .transition({ // 设置Image转场动画
      type: TransitionType.Insert, // 设置Image的入场动画
      scale: CommonConstants.COMPONENT_TRANSITION_SCALE_OPEN,
      opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
    })
    .transition({ // 设置Image转场动画
      type: TransitionType.Delete, // 设置Image的退场动画
      scale: CommonConstants.COMPONENT_TRANSITION_SCALE_CLOSE,
      opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
    })
    .onClick(this.onDialogClick)
  }
}
  • StudentListPage.ets中声明一个状态来控制是否显示自定义弹框Dialog:
js 复制代码
import DataModel from '../viewmodel/DataModel';
import StudentListItem from '../view/StudentListItem';
import router from '@ohos.router';
import prompt from '@system.prompt';
import { DataItemBean } from '../viewmodel/DataItemBean';
import CommonConstants from '../common/constants/CommonConstants';
import { StudentCardDialog } from '../view/StudentCardDialog';
import { PreferencesUtil } from '../common/db/PreferencesUtil';
import Logger from '../common/utils/Logger';
import { CustomStudentCardDialog } from '../view/CustomStudentCardDialog';
// import DataItemBean from '../viewmodel/DataItemBean';

const TAG = '[StudentListPage]';

@Entry
@Component
export struct StudentListPage {
  
  ...

  build() {
    Navigation() {
      Stack() {
        Row() {
          if (this.isListModel) {
            Scroll() {
              Column() {
                // Swiper组件
                this.SwiperBuilder(this.studentList2)
                // 列表
                List({ space: 16 }) {
                  ForEach(this.studentList2, (item: DataItemBean, index: number) => {
                    ListItem() {
                      StudentListItem({
                        studentData: item,
                        index: index,
                        isListModel: this.isListModel,
                        clickIndex: $clickIndex, // 带有"@Link"装饰的属性必须初始化为"$"
                        onItemChildImageClick: () => {
                          this.currentClickItemData = item

                          // 使用组内转场动画自定义Dialog
                          // 组件内转场动画需要配合 animateTo 才能生效,动效时长、曲线、延时跟随 animateTo 中的配置
                          // 📢:组件内的转场动画时长,动画曲线等动画参数以 animationTo 方法设置的为基准
                          // 显示动画-添加展开动画
                          animateTo({ duration: 1000 }, () => {
                            this.isShowDialog = !this.isShowDialog ;
                          })
                        }
                      })
                    }
                  }, (item, index) => JSON.stringify(item) + index)
                }
                .divider({ strokeWidth: 1, color: Color.Gray, startMargin: 30, endMargin: 0 })

                Text('---没有更多了---').fontSize('16vp').margin('30vp')
              }
            }
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
          } else {

            ...

          }
        }
        .width('90%')

        // 自定义图片弹框布局
        if (this.isShowDialog){
          CustomStudentCardDialog({
            studentData: this.currentClickItemData,
            onDialogClick: () => {
              // 组件内转场动画需要配合 animateTo 才能生效,动效时长、曲线、延时跟随 animateTo 中的配置
              // 📢:组件内的转场动画时长,动画曲线等动画参数以 animationTo 方法设置的为基准
              // 显示动画-添加展开动画
              animateTo({ duration: 1000 }, () => {
                this.isShowDialog = !this.isShowDialog ;
              })
            }
          })
        }
      }

    }
    .title('学生名单')
    .size({ width: '100%', height: '100%' })
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    .menus(this.NavigationMenus())
    .backgroundColor($r('app.color.page_background'))
  }
}
注意:组件内转场动画需要配合 animateTo 才能生效,动效时长、曲线、延时跟随 animateTo 中的配置。
在上面弹框动画的基础上加码实现:

可以看到增加了沿着Y轴旋转的动画效果:

关键代码:
typescript 复制代码
import CommonConstants from '../common/constants/CommonConstants';
import { DataItemBean } from '../viewmodel/DataItemBean';

@Component
export struct CustomStudentCardDialog {
  private studentData: DataItemBean;
  onDialogClick: () => void

  build() {
    // dialog默认的borderRadius为24vp,如果需要使用border属性,请和borderRadius属性一起使用。
    Row() {
      RelativeContainer() {
        Image(this.studentData.image)
          .objectFit(ImageFit.Cover)
          .width('300vp')
          .height('450vp')
          .borderRadius(24)
          .alignRules({
            top: { anchor: "__container__", align: VerticalAlign.Top },
            left: { anchor: "__container__", align: HorizontalAlign.Start }
          })
          .id("image")

        Text(this.studentData.title)
          .fontSize('18fp')
          .fontColor(Color.White)
          .backgroundColor($r('app.color.transparent_backgroundColor'))
          .borderRadius({ topLeft: 24, bottomRight: 24 })
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .alignRules({
            bottom: { anchor: "image", align: VerticalAlign.Bottom },
            right: { anchor: "image", align: HorizontalAlign.End }
          })
          .id("text")
      }
      .width('300vp')
      .height('450vp')
    }
    .justifyContent(FlexAlign.Center)
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .height('100%')
    // 组内转场动画
    .transition({ // 设置Image转场动画
      type: TransitionType.Insert, // 设置Image的入场动画
      scale: CommonConstants.COMPONENT_TRANSITION_SCALE_OPEN,
      rotate: CommonConstants.COMPONENT_TRANSITION_ROTATE,
      opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
    })
    .transition({ // 设置Image转场动画
      type: TransitionType.Delete, // 设置Image的退场动画
      scale: CommonConstants.COMPONENT_TRANSITION_SCALE_CLOSE,
      rotate: CommonConstants.COMPONENT_TRANSITION_ROTATE,
      opacity: CommonConstants.COMPONENT_TRANSITION_OPACITY
    })
    .onClick(this.onDialogClick)
  }
}


static readonly COMPONENT_TRANSITION_ROTATE: ComponentTransitionRotate = {
    x: 0, y: 1, z: 0, angle: 720
};

二、共享元素转场动画

共享元素转场动画指的是组件支持页面间的转场,在页面切换时有一个共享的组件在新旧两个页面之间切换,因为共享的组件在新旧页面上的位置、尺寸等有所差异,所以在页面切换时它会从旧页面逐渐过渡到新页面中的指定位置,这样就会产生了转场动画。

当路由进行切换时,可以通过设置组件的 sharedTransition 属性将该元素标记为共享元素并设置对应的共享元素转场动效。

typescript 复制代码
declare class CommonTransition<T> {
  sharedTransition(id: string, options?: sharedTransitionOptions): T;
}
  • id :设置共享元素转场动画的唯一标识,两个页面的组件配置为同一个id,则转场过程中会进行共享元素转场,配置为空字符串时不会有共享元素转场效果。

  • options :设置共享元素转场动画的配置参数, sharedTransitionOptions 参数说明如下:

    • duration:设置动画的执行时长,单位为毫秒,默认为 1000 毫秒。
    • curve :设置动画曲线,默认值为 Linear
    • delay:设置动画的延迟执行时长,单位为毫秒,默认为 0 不延迟。
    • motionPath:自定义动画路径。
    • type:设置动画类型。
实现效果:
代码实现:

StudentDetailPage.ets中:

typescript 复制代码
import router from '@ohos.router'
import CommonConstants from '../common/constants/CommonConstants';
import Logger from '../common/utils/Logger';
import { DataItemBean } from '../viewmodel/DataItemBean';

const TAG = '[StudentDetailPage]';

@Entry
@Component
struct StudentDetailPage {

  ...

  build() {
    Column({ space: 16 }) {

      ...

      Image(this.studentData.image)
        .objectFit(ImageFit.Contain)
        .backgroundColor(Color.Pink)
        .width(360)
        .height(200)
        .border({ width: 2 })
        .borderColor(Color.Blue)
        .borderRadius(10)
        .borderStyle(BorderStyle.Dotted)
        .onComplete(() => {
          this.isShowLoadingProgress = false
        })
        // 共享元素转场:通过修改共享元素的sharedTransition属性设置元素在不同页面之间过渡动效。
        // 例如,如果两个页面使用相同的图片(但位置和大小不同),图片就会在这两个页面之间流畅地平移和缩放。
        .sharedTransition(CommonConstants.SHARE_TRANSITION_ID_STUDENT_IMAGE, {
          duration: CommonConstants.TRANSITION_ANIMATION_DURATION,
          curve: Curve.Smooth,
          delay: CommonConstants.SHARE_ITEM_ANIMATION_DELAY
        })
        .onClick(() => {
          router.pushUrl({
            url: CommonConstants.BIG_IMAGE_URL,
            params: {
              imageUrl: this.studentData.image
            }
          }).catch((error) => {
            Logger.error('Push BigImagePage error: ' + JSON.stringify(error))
          })
        })


      ...

    }
    .width('100%')
    .height('100%')
    .padding('16vp')
    .backgroundColor($r('app.color.page_background'))
  }
}

BigImagePage.ets:

typescript 复制代码
import { DataItemBean } from '../viewmodel/DataItemBean'
import router from '@ohos.router'
import CommonConstants from '../common/constants/CommonConstants'

@Entry
@Component
struct BigImagePage {

  // 使用router.getParams()获取页面传递进来的的数据
  // @State studentData: DataItemBean = router.getParams()['studentData']
  @State imageUrl: string = router.getParams()['imageUrl']

  build() {
    Column() {
      // Image(this.studentData.image)
      Image(this.imageUrl)
        .objectFit(ImageFit.Contain)
          // 共享元素转场:通过修改共享元素的sharedTransition属性设置元素在不同页面之间过渡动效。
          // 例如,如果两个页面使用相同的图片(但位置和大小不同),图片就会在这两个页面之间流畅地平移和缩放。
        .sharedTransition(CommonConstants.SHARE_TRANSITION_ID_STUDENT_IMAGE, {
          duration: CommonConstants.TRANSITION_ANIMATION_DURATION,
          curve: Curve.Smooth,
          delay: CommonConstants.SHARE_ITEM_ANIMATION_DELAY
        })
    }
    .onClick(() => {
      router.back()
    })
  }
}

共用的的id:

typescript 复制代码
 static readonly SHARE_TRANSITION_ID_STUDENT_IMAGE = 'studentImage';

三、页面间转场动画

页面转场动画是指页面在打开或者关闭时添加的动画效果,它是通过在全局pageTransition方法内配置页面入场组件PageTransitionEnter和页面退场组件PageTransitionExit来自定义页面转场动效。

1、页面入场和退场

typescript 复制代码
// 入场动效接口定义
interface PageTransitionEnterInterface extends CommonTransition<PageTransitionEnterInterface> {
  (value: { type?: RouteType; duration?: number; curve?: Curve | string; delay?: number }): PageTransitionEnterInterface;
  onEnter(event: (type?: RouteType, progress?: number) => void): PageTransitionEnterInterface;
}

// 退场动效接口定义
interface PageTransitionExitInterface extends CommonTransition<PageTransitionExitInterface> {
  (value: { type?: RouteType; duration?: number; curve?: Curve | string; delay?: number }): PageTransitionExitInterface;
  onExit(event: (type?: RouteType, progress?: number) => void): PageTransitionExitInterface;
}
  • type :设置页面的路由类型,RouteType 说明如下:

    • None:没有样式
    • Push:PageA 跳转到 PageB 时,PageA 为 Exit + Push,PageB 为 Enter + Push。
    • Pop:PageB 返回至 PageA 时,PageA 为 Enter + Pop, PageB 为 Exit + Pop。
  • duration:设置动画执行时间,单位毫秒,默认为 0 。

  • curve :动画曲线,默认值为 Linear

  • onEnter:页面入场时的事件回调,其中 progress 取值范围为:[0 ~ 1]。

  • onExit:页面入场时的事件回调,其中 progress 取值范围为:[0 ~ 1]。

2、页面间转场动画属性

入场动效 PageTransitionEnter 和退场动效 PageTransitionExit 都间接继承自 CommonTransition

typescript 复制代码
declare class CommonTransition<T> {
  slide(value: SlideEffect): T;
  translate(value: { x?: number | string; y?: number | string; z?: number | string }): T;
  scale(value: { x?: number; y?: number; z?: number; centerX?: number | string; centerY?: number | string }): T;
  opacity(value: number): T;
}
  • slide :设置页面入场或者退场的方向效果,SlideEffect 说明如下:

    • Left:设置到入场时表示从左边滑入,出场时表示滑出到左边。
    • Right:设置到入场时表示从右边滑入,出场时表示滑出到右边。
    • Top:设置到入场时表示从上边滑入,出场时表示滑出到上边。
    • Bottom:设置到入场时表示从下边滑入,出场时表示滑出到下边。
  • translate :设置页面转场时的平移效果,为入场时起点和退场时终点的值,和 slide 同时设置时默认生效 slide

  • scale:设置页面转场时的缩放效果,为入场时起点和退场时终点的值。

  • opacity: 设置入场的起点透明度值或者退场的终点透明度值。

3、举例

(1)、实现从底部弹出动效的页面间转场动画

代码实现:

StudentDetailPage.ets

typescript 复制代码
 static readonly TRANSITION_ANIMATION_DURATION: number = 600;


/**
 * 使用全局pageTransition方法配置页面转换参数。
 * SlideEffect.Bottom:进入时从屏幕底部滑动
 * SlideEffect.Bottom:退出时从屏幕底部滑出
 */
pageTransition() {
  // 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0% -- 100%)
  PageTransitionEnter({ duration: CommonConstants.TRANSITION_ANIMATION_DURATION, curve: Curve.Smooth })
    .slide(SlideEffect.Bottom);
  // 退场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0% -- 100%)
  PageTransitionExit({ duration: CommonConstants.TRANSITION_ANIMATION_DURATION, curve: Curve.Smooth })
    .slide(SlideEffect.Bottom);
}

(2)、实现从中心向左右两侧展开动效的页面间转场动画

代码实现:

ArticleDetailPage.ets

typescript 复制代码
import router from '@ohos.router'
import { WanArticleBean } from '../common/bean/WanArticleBean'
import web_webview from '@ohos.web.webview';
import Logger from '../common/utils/Logger';
import CommonConstants from '../common/constants/CommonConstants';

@Entry
@Component
export default struct ArticleDetailPage {

  ...

  // 自定义方式:完全自定义转场过程的效果
  @State scale2: number = 1
  @State opacity2: number = 1

  /**
   * 自定义方式:完全自定义转场过程的效果
   *
   */
  pageTransition() {
    // 配置了当前页面的入场动画为放大,退场动画为缩小
    // 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0% -- 100%)
    PageTransitionEnter({ duration: 1200, curve: Curve.Linear })
      .onEnter((type: RouteType, progress: number) => {
        this.scale2 = progress
        this.opacity2 = progress
      });
    // 退场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0% -- 100%)
    PageTransitionExit({ duration: 1500, curve: Curve.Ease })
      .onExit((type: RouteType, progress: number) => {
        this.scale2 = 1 - progress
        this.opacity2 = 1 - progress
      });
  }

  build() {
    Stack() {

      ...

    }
    .scale({ x: this.scale2 })
    .opacity(this.opacity2)
  }
}

(3)、实现从中心向水平垂直四周方向展开动效的页面间转场动画

ArticleDetailPage.ets

js 复制代码
import router from '@ohos.router'
import { WanArticleBean } from '../common/bean/WanArticleBean'
import web_webview from '@ohos.web.webview';
import Logger from '../common/utils/Logger';
import CommonConstants from '../common/constants/CommonConstants';

@Entry
@Component
export default struct ArticleDetailPage {

  // 自定义方式:完全自定义转场过程的效果
  @State scale2: number = 1
  @State opacity2: number = 1

  /**
   * 自定义方式:完全自定义转场过程的效果
   *
   */
  pageTransition() {
    // 配置了当前页面的入场动画为放大,退场动画为缩小
    // 进场过程中会逐帧触发onEnter回调,入参为动效的归一化进度(0% -- 100%)
    PageTransitionEnter({ duration: 1200, curve: Curve.Linear })
      .onEnter((type: RouteType, progress: number) => {
        this.scale2 = progress
        this.opacity2 = progress
      });
    // 退场过程中会逐帧触发onExit回调,入参为动效的归一化进度(0% -- 100%)
    PageTransitionExit({ duration: 1500, curve: Curve.Ease })
      .onExit((type: RouteType, progress: number) => {
        this.scale2 = 1 - progress
        this.opacity2 = 1 - progress
      });
  }

  build() {
    Stack() {
      
      ...

    }
    .scale({ x: this.scale2, y: this.scale2 })
    .opacity(this.opacity2)
  }
}

总结

至此,HarmonyOS中的动画部分我们就学习结束了,结合属性动画、显示动画以及各种转场动画,我们可以将我们的页面UI变得非常丰富,页面间的跳转可以按照我们的自定义方法变得丰富多彩。 接下来,将进入我们最喜欢,应用开发中最常用的数据开发模块,敬请期待~~~

相关推荐
zhangphil3 小时前
Android绘图Path基于LinearGradient线性动画渐变,Kotlin(2)
android·kotlin
watl03 小时前
【Android】unzip aar删除冲突classes再zip
android·linux·运维
键盘上的蚂蚁-3 小时前
PHP爬虫类的并发与多线程处理技巧
android
Freerain993 小时前
鸿蒙Next类属性观测器V2
华为·harmonyos
喜欢猪猪4 小时前
Java技术专家视角解读:SQL优化与批处理在大数据处理中的应用及原理
android·python·adb
yg_小小程序员5 小时前
鸿蒙开发(16)使用DevEco Studio上的Git工具进行多远程仓管理
git·华为·harmonyos
JasonYin~6 小时前
HarmonyOS NEXT 实战之元服务:静态案例效果---每日玩机技巧
harmonyos
轻口味6 小时前
【每日学点鸿蒙知识】多线程限制、axios组件下载进度问题、lpx问题、Web组件全局代理、ArrayList问题
华为·harmonyos
yuanlaile6 小时前
纯Dart Flutter库适配HarmonyOS
flutter·华为·harmonyos·flutter开发鸿蒙·harmonyos教程
yuanlaile6 小时前
Flutter开发HarmonyOS 鸿蒙App的好处、能力以及把Flutter项目打包成鸿蒙应用
flutter·华为·harmonyos·flutter开发鸿蒙