HarmonyOS 转场动画
转场动画是指对将要出现或消失的组件做动画,对始终出现的组件做动画应使用属性动画。转场动画主要为了让开发者从繁重的消失节点管理中解放出来,如果用属性动画做组件转场,开发者需要在动画结束回调中删除组件节点。同时,由于动画结束前已经删除的组件节点可能会重新出现,还需要在结束回调中增加对节点状态的判断。
转场动画分为基础转场和高级模板化转场,有如下几类:
-
出现/消失转场:对新增、消失的控件实现动画效果,是通用的基础转场效果。
-
模态转场:新的界面覆盖在旧的界面之上的动画,旧的界面不消失,新的界面出现,如弹框就是典型的模态转场动画。
-
共享元素转场 (一镜到底):共享元素转场是一种界面切换时对相同或者相似的元素做的一种位置和大小匹配的过渡动画效果。
-
旋转屏动画:旋转屏动画主要分为两类:布局切换的旋转屏动画和透明度变化的旋转屏动画,旨在实现屏幕显示方向变化时的自然过渡。
-
导航转场:页面的路由转场方式,对应一个界面消失,另外一个界面出现的动画效果,如设置应用一级菜单切换到二级界面。
-
页面转场动画(不推荐):页面的路由转场方式,可以通过在pageTransition函数中自定义页面入场和页面退场的转场动效。为了实现更好的转场效果,推荐使用导航转场和模态转场。
不推荐:页面转场动画 简单改了 router跳转页面加动画 (优先讲的目的是为了关闭默认的跳转动画) 了解
高频:出现/消失转场、模态转场、共享元素转场(一镜到底)
导航专场:也就是Navigation加动画(高频 又少频)
transition是基础的组件转场接口,用于实现一个组件出现或者消失时的动画效果。可以通过TransitionEffect对象的组合使用,定义出各式效果。
表1 转场效果接口
| 转场效果 | 说明 | 动画 |
|---|---|---|
| IDENTITY | 禁用转场效果。 | 无。 |
| OPACITY | 默认的转场效果,透明度转场。 | 出现时透明度从0到1,消失时透明度从1到0。 |
| SLIDE | 滑动转场效果。 | 出现时从窗口左侧滑入,消失时从窗口右侧滑出。 |
| translate | 通过设置组件平移创建转场效果。 | 出现时为translate接口设置的值到默认值0,消失时为默认值0到translate接口设置的值。 |
| rotate | 通过设置组件旋转创建转场效果。 | 出现时为rotate接口设置的值到默认值0,消失时为默认值0到rotate接口设置的值。 |
| opacity | 通过设置透明度参数创建转场效果。 | 出现时为opacity设置的值到默认透明度1,消失时为默认透明度1到opacity设置的值。 |
| move | 通过TransitionEdge创建从窗口哪条边缘出来的效果。 | 出现时从TransitionEdge方向滑入,消失时滑出到TransitionEdge方向。 |
| asymmetric | 通过此方法组合非对称的出现消失转场效果。- appear:出现转场的效果。- disappear:消失转场的效果。 | 出现时采用appear设置的TransitionEffect出现效果,消失时采用disappear设置的TransitionEffect消失效果。 |
| combine | 组合其他TransitionEffect。 | 组合其他TransitionEffect,一起生效。 |
| animation | 定义转场效果的动画参数:- 如果不定义会跟随animateTo的动画参数。- 不支持通过控件的animation接口配置动画参数。- TransitionEffect中animation的onFinish不生效。 | 调用顺序时从上往下,上面TransitionEffect的animation也会作用到下面TransitionEffect。 |
- 语法概括
组件名().transition(数据)
TransitionEffect.OPACITY.animation({ duration: 2000, curve: Curve.Ease }
TransitionEffect.rotate({ z: 1, angle: 180 })
或
TransitionEffect.rotate({ z: 1, angle: 180 }).animation({ duration: 1000 })
单个:组件名().transition(数据)
组合:组件名().transition(数据.combine(数据))
指定:TransitionEffect.asymmetric( 出现数据, 隐藏数据 )
6.0 页面转场(不推荐)
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-page-transition-animation
不总结语法规则,不记语法,看一下大致作用就行
关闭router跳转过渡
// 去掉向左的默认转场
pageTransition() {
PageTransitionExit({ duration:0 }) // 退出当前页效
}
6.1 出现/消失转场
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-enter-exit-transition
- 示例代码
@Entry
@Component
struct Index {
@State show:boolean = true
build() {
Column() {
Button('切换').onClick(() => this.show = !this.show)
if (this.show) {
Text().width(100).height(100).backgroundColor(Color.Red)
.transition(
TransitionEffect.rotate({angle:180}).animation({duration:3000})
// TransitionEffect.OPACITY.animation({duration:3000})
// .combine( TransitionEffect.rotate({angle:180}).animation({duration:3000}))
// .combine( TransitionEffect.SLIDE.animation({duration:1000}))
// .combine( TransitionEffect.translate({y:300}).animation({duration:3000}))
)
Text().width(100).height(100).backgroundColor(Color.Green).transition(
TransitionEffect.asymmetric(
// 展示 入场
TransitionEffect.rotate({angle:180}).animation({duration:1000}),
// 隐藏 退出
TransitionEffect.OPACITY.animation({duration:1000}),
)
)
}
}
}
}
- 实战案例:海滩拾贝(切记涉及到超纲的弹窗 所以不要全部看 重点看那 dialog1变量 149-166行 )
https://developer.huawei.com/consumer/cn/forum/topic/0202105744664760589?fid=0101587866109860105
import { ComponentContent, promptAction, UIContext } from '@kit.ArkUI';
export class PromptActionClass {
constructor(ctx: UIContext, contentNode: ComponentContent<Object>, options: promptAction.BaseDialogOptions) {
this.ctx = ctx;
this.contentNode = contentNode;
this.options = options;
}
ctx: UIContext;
contentNode: ComponentContent<Object>;
options: promptAction.BaseDialogOptions;
openDialog() {
if (this.contentNode !== null) {
this.ctx.getPromptAction().openCustomDialog(this.contentNode, this.options)
}
}
closeDialog(node:ComponentContent<Object>) {
if (this.contentNode !== null) {
this.ctx.getPromptAction().closeCustomDialog(this.contentNode)
}
}
updateDialog(options: promptAction.BaseDialogOptions) {
if (this.contentNode !== null) {
this.ctx.getPromptAction().updateCustomDialog(this.contentNode, options)
}
}
}
interface GoodsItemType {
imgSrc: string
imgBgpX: number
imgBgpY: number
}
class Params {
constructor(position: number, title: string, content: string) {
this.position = position;
this.title = title;
this.content = content;
}
position: number
title: string
content: string
}
const imgPosition: number[][] = [
[-10, 0],
[-136, 0],
[20, -80],
[-62, -80]
]
@Builder
function buildText(params: Params) {
Column({ space: 20 }) {
Text('')
.width(80)
.height(80)
.backgroundImagePosition({ x: imgPosition[params.position][0], y: imgPosition[params.position][1] })
.backgroundImageSize({ width: 240 })
.backgroundImage('http://tmp00002.zhaodashen.cn/hn_stsb_all.png')
Text(`你捡到了一个${params.title}`)
.fontSize(30)
.fontWeight(600)
Text(params.content)
.fontSize(20)
.textAlign(TextAlign.Center)
Button('收下宝贝')
.backgroundColor('#fba404')
.onClick(() => {
})
}
.borderRadius(16)
.width('80%')
.padding(15)
.backgroundColor('#d9ffffff')
.scale({ x: 1,y:1 })
}
@Builder
function buildList(list: Params[]) {
Column() {
List({ space: 15 }) {
ForEach(list, (item: Params) => {
ListItem() {
Row() {
Text('')
.width(80)
.height(80)
.backgroundImagePosition({ x: imgPosition[item.position][0], y: imgPosition[item.position][1] })
.backgroundImageSize({ width: 240 })
.backgroundImage('http://tmp00002.zhaodashen.cn/hn_stsb_all.png')
Column() {
Text(item.title)
.fontSize(40)
.fontWeight(600)
Text(item.content)
.fontSize(20)
}
}
}
})
}
}
.width('80%')
.padding(15)
.backgroundColor('#d9ffffff')
.borderRadius(16)
}
@Entry
@Component
struct Index {
private defaultParams1: Params = new Params(0, '', '');
private defaultParams2: Params[] = [];
private contentNode: ComponentContent<Object> =
new ComponentContent(this.getUIContext(), wrapBuilder(buildText), this.defaultParams1)
private listNode: ComponentContent<Object> =
new ComponentContent(this.getUIContext(), wrapBuilder(buildList), this.defaultParams2)
@State goods: GoodsItemType[] = [
{ imgSrc: 'http://tmp00002.zhaodashen.cn/hn_stsb_starfish.png', imgBgpX: 240, imgBgpY: 180 },
{ imgSrc: 'http://tmp00002.zhaodashen.cn/hn_stsb_seashell.png', imgBgpX: 40, imgBgpY: 350 },
{ imgSrc: 'http://tmp00002.zhaodashen.cn/hn_stsb_conch.png', imgBgpX: 120, imgBgpY: 220 },
{ imgSrc: 'http://tmp00002.zhaodashen.cn/hn_stsb_seashell2.png', imgBgpX: 180, imgBgpY: 450 },
]
@State openCustomDialog: Params[] = [
new Params(0, '海星', '派大星是你吗,这就带你去找海绵宝宝'),
new Params(1, '贝壳', '是一个很漂亮的贝壳耶,快收藏好,别被别人抢了'),
new Params(2, '海螺', '活生生的海螺被你捡起来了,要不回家炖了'),
new Params(3, '贝壳', '这个贝壳颜色不一样耶,有人喜欢么'),
]
@State collect: string[] = []
@State goodsDescription: Params[] = []
dialog1: PromptActionClass = new PromptActionClass(this.getUIContext(), this.contentNode, {
alignment: DialogAlignment.Top,
offset: { dx: 0, dy: 200 },
maskColor: Color.Transparent,
transition: TransitionEffect.asymmetric(
//开启动画
TransitionEffect.OPACITY.animation({ duration: 1000 }).combine(
TransitionEffect.scale({ x: 0 })
.animation({ duration: 1000 })
.combine(TransitionEffect.translate({ y: 180 }).animation({ duration: 1000 }))),
//关闭动画
TransitionEffect.OPACITY.animation({ delay: 500, duration: 500 }).combine(
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.animation({ duration: 1000 })
.combine(TransitionEffect.translate({ x: -100, y: -250 })))
),
isModal: true,
})
dialog2: PromptActionClass = new PromptActionClass(this.getUIContext(), this.listNode, {
alignment: DialogAlignment.Top,
offset: { dx: 0, dy: 120 },
maskColor: Color.Transparent,
transition: TransitionEffect.asymmetric(
//开启动画
TransitionEffect.OPACITY.animation({ duration: 1000 }).combine(
TransitionEffect.scale({ x: 0,y:0 })
.animation({ duration: 1000 })
.combine(TransitionEffect.translate({x:-80, y: 40 }).animation({ duration: 1000 }))),
//关闭动画
TransitionEffect.OPACITY.animation({ delay: 500, duration: 500 }).combine(
TransitionEffect.scale({ x: 0.5, y: 0.5 })
.animation({ duration: 1000 })
.combine(TransitionEffect.translate({ x: -100, y: -250 })))
),
isModal: true,
})
aboutToAppear() {
}
build() {
Column() {
Image('http://tmp00002.zhaodashen.cn/hn_stsb_driftingBottles.png')
.width(60)
.position({ x: 10, y: 20 })
.onClick(() => {
this.listNode.update(this.goodsDescription)
this.dialog2.openDialog();
})
ForEach(this.goods, (item: GoodsItemType, index: number) => {
Image(item.imgSrc)
.width(60)
.opacity(this.collect.some(i => i === item.imgSrc) ? 0 : 1)
.transition(TransitionEffect.asymmetric(
TransitionEffect.OPACITY.animation({ duration: 1000 }),
TransitionEffect.OPACITY.animation({ duration: 1000 })
))
.position({ x: item.imgBgpX, y: item.imgBgpY })
.onClick(() => {
animateTo({ duration: 1000 }, () => {
this.collect.push(item.imgSrc)
})
this.goodsDescription.push(this.openCustomDialog[index])
this.contentNode.update(this.openCustomDialog[index])
this.dialog1.openDialog();
})
})
}.zIndex(-1)
.width('100%')
.height('100%')
.backgroundImage('http://tmp00002.zhaodashen.cn/hn_stsb_background_beach.jpg')
.backgroundImageSize(ImageSize.Cover)
}
}
6.2 模态转场
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-modal-transition
6.3 共享元素转场 (一镜到底)
- 简介
共享元素转场是一种界面切换时对相同或者相似的两个元素做的一种位置和大小匹配的过渡动画效果,也称一镜到底动效。
如下例所示,在点击图片后,该图片消失,同时在另一个位置出现新的图片,二者之间内容相同,可以对它们添加一镜到底动效。左图为不添加一镜到底动效的效果,右图为添加一镜到底动效的效果,一镜到底的效果能够让二者的出现消失产生联动,使得内容切换过程显得灵动自然而不生硬。
一镜到底的动效有多种实现方式,在实际开发过程中,应根据具体场景选择合适的方法进行实现。
场景1:列表到详情
场景2:个人中心
![]()
![]()
![]()
![]()
- 使用步骤
页面1
import { router } from '@kit.ArkUI' @Entry @Component struct Index { build() { Column(){ Image($r('app.media.startIcon')).width(80) .sharedTransition('picture',{duration:2000,delay:200,curve:Curve.Linear}) Text('张三').onClick(()=>{ router.pushUrl({ url:'pages/Test' }) }) .fontSize(50) }.width('100%') } //去掉向左的默认转场 pageTransition() { PageTransitionExit({ duration:0 }) } }页面2
import { router } from '@kit.ArkUI' @Entry @Component struct Index2 { build() { Column() { Image($r('app.media.startIcon')).width(200) .margin({top: 300}) .sharedTransition('picture', { duration: 2000, delay: 200, curve: Curve.Linear }) .onClick(() => { router.back() }) } } }
- 实战场景:多元素一镜到底记得加唯一标识
页面1
import { router } from '@kit.ArkUI' @Entry @Component struct Index { private imgSrc: string[] = [ 'https://ts1.tc.mm.bing.net/th/id/R-C.987f582c510be58755c4933cda68d525?rik=C0D21hJDYvXosw&riu=http%3a%2f%2fimg.pconline.com.cn%2fimages%2fupload%2fupc%2ftx%2fwallpaper%2f1305%2f16%2fc4%2f20990657_1368686545122.jpg&ehk=netN2qzcCVS4ALUQfDOwxAwFcy41oxC%2b0xTFvOYy5ds%3d&risl=&pid=ImgRaw&r=0', 'https://tse2-mm.cn.bing.net/th/id/OIP-C.tHAIAaw4ZNlr4v2ldAkvYwHaHa?rs=1&pid=ImgDetMain', 'https://img95.699pic.com/photo/50020/5325.jpg_wh860.jpg', 'https://img-baofun.zhhainiao.com/pcwallpaper_ugc/static/bf1dfec187d0b0ad86d2a4b59f8cf847.jpg?x-oss-process=image%2fresize%2cm_lfit%2cw_1920%2ch_1080', 'https://img-baofun.zhhainiao.com/pcwallpaper_ugc_mobile/preview_jpg/f9d3e452f8f48a6e2876fecdea9ffb98.jpg', 'https://bpic.588ku.com/back_origin_min_pic/19/10/22/cb8ed894e1c0a3616c877497b3e41a82.jpg', 'https://picx.zhimg.com/v2-6ca9e1a5c977ad26a53fcc11a7ba9f57_720w.jpg?source=172ae18b', 'https://picx.zhimg.com/v2-d6f44389971daab7e688e5b37046e4e4_720w.jpg?source=172ae18b', 'https://pic1.zhimg.com/v2-02760a1bf058904006740d3f66b2c9ac_r.jpg?source=1940ef5c' ] //去掉向左的默认转场 pageTransition() { PageTransitionExit({ duration:0 }) } build() { WaterFlow() { ForEach(this.imgSrc, (item: string,index:number) => { FlowItem() { Column(){ Image(item).width('100%') .sharedTransition('picture'+index,{duration:500,delay:0,curve:Curve.Linear}) Text('太平本是将军定,不许将军见太平。窗外日光弹指过,席间花影坐前移。有缘千里来相会,无缘对面不相逢') .maxLines(2) .textOverflow({overflow:TextOverflow.Ellipsis}) } .onClick(()=>{ AppStorage.setOrCreate('index', index) AppStorage.setOrCreate('picture', item) router.pushUrl({ url:'pages/Test' }) }) } }) } .columnsTemplate('1fr 1fr') .columnsGap(10) .rowsGap(10) } }页面2
import { router } from '@kit.ArkUI' @Entry @Component struct Test { @StorageProp('index') index:number = 0 @StorageProp('picture') picture:string = '' build() { Column() { Image(this.picture).width('100%').height(300) .sharedTransition('picture'+this.index, { duration: 500, delay: 0, curve: Curve.Linear }) .onClick(() => { router.back() }) } } }
6.4 旋转屏动画 了解
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rotation-transition-animation
6.5 导航转场 Navigation
- 准备
Index.ets
@Entry
@Component
struct Index {
@Provide pageStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.pageStack) {
Text('我是main')
Button('跳转').onClick(() => {
this.pageStack.pushPathByName('Index2', null)
})
}
.hideTitleBar(true)
}
}
Index2.ets
@Builder
function PageTwoBuilder(){
Index2()
}
@Component
struct Index2 {
build() {
NavDestination() {
Column() {
Button('返回Index界面')
.width('80%')
.height(40)
.margin(20)
}.size({ width: '100%', height: '100%' })
}
.title('这是Index2界面')
.backgroundColor('#ff11dee5')
}
}
resources/base/route_map.json
{
"routerMap": [
{
"name": "Index2",
"pageSourceFile": "src/main/ets/pages/Index2.ets",
"buildFunction": "PageTwoBuilder",
"data": {
"description" : "this is PageTwo"
}
}
]
}
module.json5
"routerMap": "$profile:route_map",
- NavDestination系统转场
Navigation有一个退场动画,会影响查看NavDestination页面系统转场效果,
如果想去掉需要在Navigation重写退出转场动画或者 Navigation加载Index2界面, 然后Index2跳转Index3 (推荐算了,直接看大致效果)
@Builder
function PageTwoBuilder(){
Index2()
}
@Component
struct Index2 {
@Consume pageStack:NavPathStack
build() {
NavDestination() {
Column() {
Text('Index2')
}.size({ width: '100%', height: '100%' })
}
.title('aaa')
.backgroundColor('#ff11dee5')
.systemTransition(NavigationSystemTransitionType.SLIDE_BOTTOM)
}
}
- NavDestination自定义转场
NavDestination支持自定义转场动画,通过设置customTransition属性即可实现单个页面的自定义转场效果。要实现这一功能,需完成以下步骤:
- 实现NavDestination的转场代理,针对不同的堆栈操作类型返回自定义的转场协议对象NavDestinationTransition。其中,event是必填参数,需在此处编写自定义转场动画的逻辑;而onTransitionEnd、duration、curve与delay为可选参数,分别对应动画结束后的回调、动画持续时间、动画曲线类型与开始前的延时。若在转场代理中返回多个转场协议对象,这些动画效果将逐层叠加。
- 通过调用NavDestination组件的customTransition属性,并传入上述实现的转场代理,完成自定义转场的设置。
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-navigation-navigation#自定义转场
以下示例主要演示NavDestination设置自定义转场动画属性customTransition的效果。
@Builder
function PageTwoBuilder(){
Index2()
}
declare type voidFunc = () => void;
@Component
struct Index2 {
@State name: string = 'NA';
@State destWidth: string = '100%';
stack: NavPathStack = new NavPathStack();
@State y: string = '0';
build() {
NavDestination() {
Column() {
Button('push next page', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.stack.pushPath({ name: this.name == 'PageOne' ? "PageTwo" : "PageOne" });
})
}
.size({ width: '100%', height: '100%' })
}
.title(this.name)
.translate({ y: this.y })
.onReady((context) => {
this.name = context.pathInfo.name;
this.stack = context.pathStack;
})
.backgroundColor(this.name == 'PageOne' ? '#F1F3F5' : '#ff11dee5')
.customTransition(
(op: NavigationOperation, isEnter: boolean)
// op 1-PUSH页面进入、2-POP页面退出、3-页面替换
// isEnter 入场页面?
: Array<NavDestinationTransition> | undefined => {
console.log('[NavDestinationTransition]', 'reached delegate in frontend, op: ' + op + ', isEnter: ' + isEnter);
let transitionOneEvent: voidFunc = () => { console.log('[NavDestinationTransition]', 'reached transitionOne, empty now!'); }
let transitionOneFinishEvent: voidFunc = () => { console.log('[NavDestinationTransition]', 'reached transitionOneFinish, empty now!'); }
let transitionOneDuration: number = 500;
if (op === NavigationOperation.PUSH) {
if (isEnter) {
// ENTER_PUSH
this.y = '100%';
transitionOneEvent = () => {
console.log('[NavDestinationTransition]', 'transitionOne, push & isEnter');
this.y = '0';
}
} else {
// EXIT_PUSH
this.y = '0';
transitionOneEvent = () => {
console.log('[NavDestinationTransition]', 'transitionOne, push & !isEnter');
this.y = '0';
}
transitionOneDuration = 450;
}
} else if (op === NavigationOperation.POP) {
if (isEnter) {
// ENTER_POP
this.y = '0';
transitionOneEvent = () => {
console.log('[NavDestinationTransition]', 'transitionOne, pop & isEnter');
this.y = '0';
}
} else {
// EXIT_POP
this.y = '0';
transitionOneEvent = () => {
console.log('[NavDestinationTransition]', 'transitionOne, pop & !isEnter');
this.y = '100%';
}
}
} else {
console.log('[NavDestinationTransition]', '----- NOT-IMPL BRANCH of NAV-DESTINATION CUSTOM TRANSITION -----');
}
let transitionOne: NavDestinationTransition = {
duration: transitionOneDuration,
delay: 0,
curve: Curve.Friction,
event: transitionOneEvent,
onTransitionEnd: transitionOneFinishEvent
};
let transitionTwoEvent: voidFunc = () => { console.log('[NavDestinationTransition]', 'reached transitionTwo, empty now!'); }
let transitionTwo: NavDestinationTransition = {
duration: 1000,
delay: 0,
curve: Curve.EaseInOut,
event: transitionTwoEvent,
onTransitionEnd: () => { console.log('[NavDestinationTransition]', 'reached Two\'s finish'); }
};
return [
transitionOne,
transitionTwo,
];
})
}
}
- Navigation自定义转场动画
- utils/CustomNavigationUtils.ets
export interface AnimateCallback {
finish: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
start: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
onFinish: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
timeout: (number | undefined) | undefined;
}
let customTransitionMap: Map<number, AnimateCallback> = new Map()
export class CustomTransition {
static delegate = new CustomTransition();
private constructor() {
}
static getInstance(): CustomTransition {
return CustomTransition.delegate;
}
// 注册某个页面的动画回调
registerNavParam(name: number, startCallback: (operation: boolean, isExit: boolean) => void,
endCallback: (operation: boolean, isExit: boolean) => void,
onFinish: (operation: boolean, isExit: boolean) => void, timeout: number): void {
if (customTransitionMap.has(name)) {
let param = customTransitionMap.get(name);
if (param !== undefined) {
param.start = startCallback;
param.finish = endCallback;
param.timeout = timeout;
param.onFinish = onFinish;
return;
}
}
let params: AnimateCallback = {
timeout: timeout,
start: startCallback,
finish: endCallback,
onFinish: onFinish
};
customTransitionMap.set(name, params);
}
unRegisterNavParam(name: number): void {
customTransitionMap.delete(name);
}
getAnimateParam(name: number): AnimateCallback {
let result: AnimateCallback = {
start: customTransitionMap.get(name)?.start,
finish: customTransitionMap.get(name)?.finish,
timeout: customTransitionMap.get(name)?.timeout,
onFinish: customTransitionMap.get(name)?.onFinish
};
return result;
}
}
export class FlowFood {
title: string
content: string
itemIndex: number
constructor(title: string, content: string, itemIndex: number) {
this.title = title
this.content = content
this.itemIndex = itemIndex
}
}
- pages/Index.ets
// Index.ets
import { AnimateCallback, CustomTransition } from '../utils/CustomNavigationUtils'
@Entry
@Component
struct Index {
pageId: number = 0;
@State number: number = 25
@State myScale: number = 1
@Provide pageStack: NavPathStack = new NavPathStack()
aboutToAppear() {
this.pageId = this.pageStack.getAllPathName().length - 1;
}
build() {
Navigation(this.pageStack) {
Text('我是main')
Button('跳转').onClick(() => {
CustomTransition.getInstance().registerNavParam(this.pageId, (_isPush: boolean, isExit: boolean) => {
this.myScale = isExit ? 1 : 1.5;
}, (_isPush: boolean, isExit: boolean) => {
this.myScale = isExit ? 1.2 : 1;
}, (_isPush: boolean, _isExit: boolean) => {
this.myScale = 1;
}, 200);
this.pageStack.pushPathByName('Index2', null)
})
}
.hideTitleBar(true)
.customNavContentTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => {
let customAnimation: NavigationAnimatedTransition = {
transition: (transitionProxy: NavigationTransitionProxy) => {
// 从CustomTransition中根据子页面的序列获取对应的转场动画回调
let fromParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(from.index);
let toParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(to.index);
if (toParam.start !== undefined) {
toParam.start(operation === NavigationOperation.PUSH, false);
}
animateTo({
duration: 400, onFinish: () => {
transitionProxy.finishTransition();
}
}, () => {
if (fromParam.finish !== undefined) {
fromParam.finish(operation === NavigationOperation.PUSH, true);
}
if (toParam.finish !== undefined) {
toParam.finish(operation === NavigationOperation.PUSH, false);
}
})
}
};
return customAnimation;
})
}
}
- Index2.ets
import { CustomTransition } from '../utils/CustomNavigationUtils'
@Builder
function PageTwoBuilder(){
Index2()
}
@Component
struct Index2 {
@Consume pageStack:NavPathStack
pageId: number = 0;
@State angle: number = 0
aboutToAppear() {
this.pageId = this.pageStack.getAllPathName().length - 1;
CustomTransition.getInstance().registerNavParam(this.pageId, (isPush: boolean, isExit: boolean) => {
this.angle = isExit ? 0 : isPush ? -90 : 0;
}, (isPush: boolean, isExit: boolean) => {
this.angle = isExit ? isPush ? 90 : -90 : 0;
}, (_isPush: boolean, _isExit: boolean) => {
this.angle = 0;
}, 2000)
}
build() {
NavDestination() {
Column() {
Text('Index2')
}.size({ width: '100%', height: '100%' })
}
.title('aaa')
.backgroundColor('#ff11dee5')
.onDisAppear(() => {
CustomTransition.getInstance().unRegisterNavParam(this.pageId)
})
.rotate({
x: 0,
y: 1,
z: 0,
centerX: '100%',
centerY: '50%',
angle: this.angle
})
}
}
- 自定义动画
import { CustomTransition } from '../utils/CustomNavigationUtils'
@Builder
function PageTwoBuilder(){
Index2()
}
@Component
struct Index2 {
@Consume pageStack:NavPathStack
pageId: number = 0;
@State w: string = '100%'
@State h: string = '100%'
@State angle: number = 0
aboutToAppear() {
this.pageId = this.pageStack.getAllPathName().length - 1;
CustomTransition.getInstance().registerNavParam(this.pageId, (isPush: boolean, isExit: boolean) => {
this.angle = isExit ? 0 : isPush ? -90 : 0;
this.w = this.h = isExit ? '100%' : isPush ? '50%' : '100%'
}, (isPush: boolean, isExit: boolean) => {
this.angle = isExit ? isPush ? 90 : -90 : 0;
this.w = this.h = isExit ? isPush ? '100%' : '50%' : '100%';
}, (_isPush: boolean, _isExit: boolean) => {
this.angle = 0;
this.w = this.h = '100%';
}, 2000)
}
build() {
NavDestination() {
Column() {
Text('Index2')
}.size({ width: '100%', height: '100%' })
}
.title('aaa')
.backgroundColor('#ff11dee5')
.onDisAppear(() => {
CustomTransition.getInstance().unRegisterNavParam(this.pageId)
})
.size({ width: this.w, height: this.h })
.rotate({
x: 0,
y: 1,
z: 0,
centerX: '100%',
centerY: '50%',
angle: this.angle
})
}
}
- Navigation一镜到底
Index.ets
// Index.ets
import { AnimateCallback, CustomTransition } from '../utils/CustomTransitionUtils'
import { display } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@Provide pageStack: NavPathStack = new NavPathStack()
@State x: number | string = 0;
@State y: number | string = '100%';
@State currentIndex: number = -1;
@State itemWidth: number = 0;
@State itemHeight: number = 0;
@State itemOffsetX: number = 0;
@State itemOffsetY: number = 0;
@State screenWidth: number = 0;
@State screenHeight: number = 0;
@State itemRealWidth: number | string = '100%';
@State itemRealHeight: number | string = '';
@State lineNum: number = -1;
private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7];
private scroller: Scroller = new Scroller();
pageId: number = 0;
aboutToAppear() {
this.pageId = this.pageStack.getAllPathName().length - 1;
CustomTransition.getInstance().registerNavParam(this.pageId, (isPush: boolean, isExit: boolean) => {
this.x = isExit ? 0 : isPush ? '100%' : 0;
}, (isPush: boolean, isExit: boolean) => {
this.x = isExit ? isPush ? 0 : '100%' : 0;
}, (_isPush: boolean, _isExit: boolean) => {
this.x = 0
}, 2000)
let displayClass: display.Display | null = null;
displayClass = display.getDefaultDisplaySync()
this.screenWidth = px2vp(displayClass.width)
this.screenHeight = px2vp(displayClass.height)
}
build() {
Navigation(this.pageStack) {
WaterFlow({ scroller: this.scroller }) {
ForEach(this.arr, (item: number, index: number) => {
FlowItem() {
Column({ space: 10 }) {
Image('https://img2.baidu.com/it/u=3134190812,2393484759&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=578')
.width('100%')
Text('小帅哥' + item)
.fontSize(20)
Text('内容信息')
.maxLines(this.currentIndex === index ? this.lineNum : 2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width(this.currentIndex === index ? this.itemRealWidth : '100%')
.height(this.currentIndex === index ? this.itemRealHeight : '')
.borderRadius(12)
.position({
x: this.currentIndex === index ? this.itemOffsetX : 0,
y: this.currentIndex === index ? this.itemOffsetY : 0
})
.backgroundColor(Color.White)
.clip(true)
.onAreaChange((_oldValue: Area, newValue: Area) => {
if (index === 0) {
this.itemWidth = newValue.width as number
this.itemHeight = newValue.height as number
}
})
.onClick(() => {
this.currentIndex = index
const ITEM_P_X: number = this.scroller.getItemRect(index).x
const ITEM_P_Y: number = this.scroller.getItemRect(index).y
this.lineNum = -1
CustomTransition.getInstance().registerNavParam(this.pageId, (isPush: boolean, isExit: boolean) => {
this.itemRealWidth = isExit ? '100%' : isPush ? '100%' : this.screenWidth
this.itemRealHeight = isExit ? '' : isPush ? '' : this.screenHeight
this.itemOffsetX = isExit ? 0 : isPush ? 0 : -ITEM_P_X
this.itemOffsetY = isExit ? 0 : isPush ? 0 : -ITEM_P_Y
}, (isPush: boolean, isExit: boolean) => {
this.itemRealWidth = isExit ? isPush ? this.screenWidth : this.screenWidth : '100%'
this.itemRealHeight = isExit ? isPush ? this.screenHeight : this.screenHeight : ''
this.itemOffsetX = isExit ? isPush ? -ITEM_P_X : -ITEM_P_X : 0
this.itemOffsetY = isExit ? isPush ? -ITEM_P_Y : -ITEM_P_Y : 0
}, (_isPush: boolean, _isExit: boolean) => {
this.itemRealWidth = '100%'
this.itemRealHeight = ''
this.itemOffsetX = 0
this.itemOffsetY = 0
this.lineNum = 2
}, 2000)
this.pageStack.pushPathByName('Index2', null)
})
}
.width('100%')
.padding({top:8})
.zIndex(this.currentIndex === index ? 2 : 1)
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr')
.columnsGap(10)
.rowsGap(5)
.scrollBar(BarState.Off)
.friction(0.6)
.edgeEffect(EdgeEffect.Spring)
.width('100%')
.height('100%')
}
.hideTitleBar(true)
.customNavContentTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => {
let customAnimation: NavigationAnimatedTransition = {
transition: (transitionProxy: NavigationTransitionProxy) => {
// 从CustomTransition中根据子页面的序列获取对应的转场动画回调
let fromParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(from.index);
let toParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(to.index);
if (toParam.start !== undefined) {
toParam.start(operation === NavigationOperation.PUSH, false);
}
animateTo({
duration: 400, onFinish: () => {
transitionProxy.finishTransition();
}
}, () => {
if (fromParam.finish !== undefined) {
fromParam.finish(operation === NavigationOperation.PUSH, true);
}
if (toParam.finish !== undefined) {
toParam.finish(operation === NavigationOperation.PUSH, false);
}
})
}
};
return customAnimation;
})
}
}
Index2.ets
import { CustomTransition } from '../utils/CustomTransitionUtils'
@Builder
function PageTwoBuilder(){
Index2()
}
@Component
struct Index2 {
@Consume pageStack:NavPathStack
@State opacityNum: number = 1
@State title: string = ''
@State content: string = ''
@State itemIndex: number = 0
pageId: number = 0;
aboutToAppear() {
this.pageId = this.pageStack.getAllPathName().length - 1;
CustomTransition.getInstance().registerNavParam(this.pageId, (isPush: boolean, isExit: boolean) => {
this.opacityNum = isExit ? 1 : isPush ? 0 : 1;
}, (isPush: boolean, isExit: boolean) => {
this.opacityNum = isExit ? isPush ? 1 : 0 : 1;
}, (_isPush: boolean, _isExit: boolean) => {
this.opacityNum = 1
}, 2000)
}
build() {
NavDestination() {
Scroll() {
Column({ space: 10 }) {
Image('https://img2.baidu.com/it/u=3134190812,2393484759&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=578')
.width('100%')
Text(this.title + this.itemIndex)
.fontSize(20)
Text(this.content)
}
}
}
.hideTitleBar(true)
.onDisAppear(() => {
CustomTransition.getInstance().unRegisterNavParam(this.pageId)
})
.opacity(this.opacityNum)
}
}



浙公网安备 33010602011771号