系列文章目录
HarmonyOS应用开发之道:掌握未来,领略技术的魅力-01-ArkTS基础知识
HarmonyOS应用开发全攻略:从入门到精通-02-程序框架UIAbility、启动模式与路由跳转
HarmonyOS应用开发系列:探索未来的技术前沿-03-基础组件-让我们来码出复杂酷炫的UI
HarmonyOS应用开发指南:为未来做好准备-04-组件状态管理
HarmonyOS应用开发必修课-05-让我们的界面动起来!- 显示动画与属性动画
HarmonyOS应用开发探索之旅:探索未来的技术前沿-06-炫酷的转场动画
前言
本节记录学习下HamronyOS中的动画,运用到组件动画、转场动画等场景,让我们的UI界面炫酷起来~
先上效果图:
共享元素转场动画 | 系统弹窗 | 组件内转场动画1 | 组件内转场动画2 |
---|---|---|---|
组件内转场动画3 | 页面间转场动画1 | 页面间转场动画2 | 页面间转场动画3 |
--- | --- | --- | --- |
回顾下动画的分类:
按页面分类:
按基础能力分类
接下来分析介绍页面转场动画
一、组内转场动画
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变得非常丰富,页面间的跳转可以按照我们的自定义方法变得丰富多彩。 接下来,将进入我们最喜欢,应用开发中最常用的数据开发模块,敬请期待~~~