HarmonyOS 转场动画

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

转场动画分为基础转场和高级模板化转场,有如下几类:

不推荐:页面转场动画 简单改了 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系统转场

https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navdestination#示例3设置指定的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属性即可实现单个页面的自定义转场效果。要实现这一功能,需完成以下步骤:

  1. 实现NavDestination的转场代理,针对不同的堆栈操作类型返回自定义的转场协议对象NavDestinationTransition。其中,event是必填参数,需在此处编写自定义转场动画的逻辑;而onTransitionEnd、duration、curve与delay为可选参数,分别对应动画结束后的回调、动画持续时间、动画曲线类型与开始前的延时。若在转场代理中返回多个转场协议对象,这些动画效果将逐层叠加。
  2. 通过调用NavDestination组件的customTransition属性,并传入上述实现的转场代理,完成自定义转场的设置。

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-navigation-navigation#自定义转场

https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navdestination#示例2设置navdestination自定义转场

以下示例主要演示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自定义转场动画

https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-components-navigation#示例13自定义转场动画

  • 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)
  }
}

鸿蒙开发者班级

posted @ 2025-11-27 12:57  神龙教主  阅读(1)  评论(0)    收藏  举报