HarmonyOS鸿蒙Next中ArkUI路由/导航系列七:Navigation自定义转场动画,让页面切换炫起来

HarmonyOS鸿蒙Next中ArkUI路由/导航系列七:Navigation自定义转场动画,让页面切换炫起来

1、Navigation自定义动画

1.1 准备工作

开发者需要声明自定义动画的管理类,同时创建全局单实例的CustomTransition,以便于管理自定义动画的参数注册和获取,如下是一个简单的示例,开发者也可根据自己的业务需求去实现:

// CustomNavigationUtils.ts 工具类,用来管理所有页面的自定义动画参数注册和获取等

// 自定义接口,用来保存某个页面相关的转场动画回调和参数
export interface AnimateCallback {
  start: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
  finish: ((isPush: boolean, isExit: boolean) => void | undefined) | undefined;
}

const customTransitionMap: Map<string, AnimateCallback> = new Map();

export class CustomTransition {
  static delegate = new CustomTransition();

  static getInstance() {
    return CustomTransition.delegate;
  }

  /* 注册某个页面的动画回调
   * name: 注册页面的唯一id
   * startCallback:用来设置动画开始时页面的状态
   * endCallback:用来设置动画结束时页面的状态
   */
  registerNavParam(name: string, startCallback: (isPush: boolean, isExit: boolean) => void,
    endCallback: (isPush: boolean, isExit: boolean) => void): void {
    if (customTransitionMap.has(name)) {
      let param = customTransitionMap.get(name);
      if (param != undefined) {
        param.start = startCallback;
        param.finish = endCallback;
        return;
      }
    }
    let params: AnimateCallback = { start: startCallback, finish: endCallback };
    customTransitionMap.set(name, params);
  }

  unRegisterNavParam(name: string): void {
    customTransitionMap.delete(name);
  }

  getAnimateParam(name: string): AnimateCallback {
    let result: AnimateCallback = {
      start: customTransitionMap.get(name)?.start,
      finish: customTransitionMap.get(name)?.finish
    };
    return result;
  }
}

1.2 代码实现

对于Navigation组件,需要设置属性customNavContentTransition,该属性需要传入NavigationAnimatedTransition类型的参数delegate。在路由操作发生时,系统侧会回调至delegate,并将页面切换的from、to以及操作类型信息返回给该函数作为入参。开发者可以在delete中根据系统回传的信息,来决定执行什么类型的自定义动画,如下示例:

// NavigationCustomAniMain.ets

@Component
struct NavigationCustomTransitionExample {
  pageInfos: NavPathStack = new NavPathStack();

  aboutToAppear() {
    this.pageInfos.pushPath({ name: 'navigationAniPageOne' }, false);
  }

  build() {
    Navigation(this.pageInfos) {
    }
    .hideNavBar(true)
    // 实现属性customNavContentTransition,以定义动画的规则
    .customNavContentTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => {
      // 首页不进行自定义动画
      if (from.index === -1 || to.index === -1) {
        return undefined;
      }

      let customAnimation: NavigationAnimatedTransition = {
        timeout: 2000,
        // 转场开始时系统调用该方法,并传入转场上下文代理对象
        transition: (transitionProxy: NavigationTransitionProxy) => {
          if (!from.navDestinationId || !to.navDestinationId) {
            return;
          }
          // 从封装类CustomTransition中根据子页面的序列获取对应的转场动画回调
          let fromParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(from.navDestinationId);
          let toParam: AnimateCallback = CustomTransition.getInstance().getAnimateParam(to.navDestinationId);
          // Push动画
          if (operation == NavigationOperation.PUSH) {
            if (fromParam.start && toParam.start) {
              // 设置Push转场的两个页面的动画起点
              fromParam.start(true, true);
              toParam.start(true, false);
            }
            this.getUIContext()?.animateTo({
              duration: 500, curve: Curve.Friction, onFinish: () => {
                // 动画结束后需要手动调用finishTransition,否则在timeout时间后由系统调用
                transitionProxy.finishTransition();
              }
            }, () => {
              if (fromParam.finish && toParam.finish) {
                // 设置Push转场的两个页面的动画终点
                fromParam.finish(true, true);
                toParam.finish(true, false);
              }

            })
          } else if (operation == NavigationOperation.POP) {
            // Pop动画
            if (fromParam.start && toParam.start) {
              // 设置Pop转场的两个页面的动画起点
              fromParam.start(false, true);
              toParam.start(false, false);
            }
            this.getUIContext()?.animateTo({
              duration: 500, curve: Curve.Friction, onFinish: () => {
                // 动画结束后需要手动调用finishTransition,否则在timeout时间后由系统调用
                transitionProxy.finishTransition();
              }
            }, () => {
              if (fromParam.finish && toParam.finish) {
                // 设置Pop转场的两个页面的动画终点
                fromParam.finish(false, true);
                toParam.finish(false, false);
              }
            })
          } else {
            // Replace不做动画
          }
        }
      };
      return customAnimation;
    })
  }
}

同时,开发者需要为每个NavDestination注册页面自身的动画参数,以供Navigation.customNavContentTransition回调内部的逻辑调用,例如:

// NavigationCustomAniMain.ets

@Component
export struct PageContainer {
  pageInfos: NavPathStack = new NavPathStack();
  @State translateY: string = '0';
  pageId: string = '';
  title: string = ''

  registerCallback() {
    CustomTransition.getInstance().registerNavParam(this.pageId,
      // 设置转场动画起点,根据不同的转场类型分别设置
      (isPush: boolean, isExit: boolean) => {
        if (isPush) {
          if (isExit) {
            this.translateY = '0';
          } else {
            this.translateY = '100%';
          }
        } else {
          if (isExit) {
            this.translateY = '0';
          } else {
            this.translateY = '0';
          }
        }
      },
      // 设置转场动画终点,根据不同的转场类型分别设置
      (isPush: boolean, isExit: boolean) => {
        if (isPush) {
          if (isExit) {
            this.translateY = '0';
          } else {
            this.translateY = '0';
          }
        } else {
          if (isExit) {
            this.translateY = '100%';
          } else {
            this.translateY = '0';
          }
        }
      });
  }

  build() {
    NavDestination() {
      Column() {
        Button('push next page')
          .width(330)
          .margin(7)
          .onClick(() => {
            this.pageInfos.pushPath({ name: this.title == 'PageOne' ? "navigationAniPageTwo" : "navigationAniPageOne" });
          })
      }
      .size({ width: '100%', height: '100%' })
    }
    .title(this.title)
    .onDisAppear(() => {
      // 页面销毁时解注册自定义转场动画参数
      CustomTransition.getInstance().unRegisterNavParam(this.pageId);
    })
    .onReady((context: NavDestinationContext) => {
      this.pageInfos = context.pathStack;
      if (context.navDestinationId) {
        this.pageId = context.navDestinationId;
        // 页面创建时注册自定义转场动画参数
        this.registerCallback();
      }
    })
    .translate({ y: this.translateY })
    .backgroundColor(this.title == 'PageOne' ? '# F1F3F5' : '# ff11dee5')
  }
}

2、NavDestination自定义动画

开发者如果仅想控制单个NavDestination的转场动画,而无需关心其他NavDestination的动画时,可以通过在NavDestination中设置customTransition属性来为当前页面配置自定义动画。该属性接收类型为NavDestinationTransitionDelegate的入参作为动画代理。与Navigation动画类似,动画代理会在页面切换时执行相,并发起相应的自定义动画,例如如下设置的NavDestination组件:

// CustomNavDest.ets

@Component
struct CustomAniNavDestination {
  name: string = 'NA'
  stack: NavPathStack = new NavPathStack()
  @State translateY: string = '0';

  build() {
    NavDestination() {
      Button('push next page')
        .width(330)
        .margin(7)
        .onClick(() => {
          this.stack.pushPath({ name: this.name == 'PageOne' ? "navDestCustomAniPageTwo" : "navDestCustomAniPageOne" });
        })
    }
    .translate({ y: this.translateY })
    .backgroundColor(this.name == 'PageOne' ? '# F1F3F5' : '# ff11dee5')
    .onReady((context) => {
      this.stack = context.pathStack
    })
    .title(this.name)
    .customTransition(
      (op: NavigationOperation, isEnter: boolean)
        : Array<NavDestinationTransition> | undefined => {
        console.log('[NavDestinationTransition]', 'reached delegate in frontend, op: ' + op + ', isEnter: ' + isEnter);
        // navDestination动画的事件。navDestination自定义动画会基于当前组件的UI状态与事件执行后的UI状态来发起动画。
        let transitionOneEvent = () => { console.log('[NavDestinationTransition]', 'reached transitionOne, empty now!') }
        if (op === NavigationOperation.PUSH) {
          if (isEnter) {
            // ENTER_PUSH
            // 为入场的PUSH页面设置动画
            console.log('[NavDestinationTransition]', 'push & isEnter, init y to 100%');
            this.translateY = '100%';
            transitionOneEvent = () => {
              console.log('[NavDestinationTransition]', 'push & isEnter, finally set y to 0');
              this.translateY = '0';
            }
          }
        } else if (op === NavigationOperation.POP) {
          if (!isEnter) {
            // EXIT_POP
            // 为离场的POP页面设置动画
            console.log('[NavDestinationTransition]', 'pop & isExit, init y to 0');
            this.translateY = '0';
            transitionOneEvent = () => {
              console.log('[NavDestinationTransition]', 'pop & isExit, finally set y to 100%');
              this.translateY = '100%';
            }
          }
        } else if (op === NavigationOperation.REPLACE) {
          console.log('[NavDestinationTransition]', 'REPLACE not support');
        } else {
          console.log('[NavDestinationTransition]', 'invalid operation!');
        }
        // 根据以上逻辑,确定动画的行为,并创建类型为NavDestinationTransition的动画实例transitionOne
        let transitionOne: NavDestinationTransition = {
          duration: 500,
          delay: 0,
          curve: Curve.Friction,
          event: transitionOneEvent,
          onTransitionEnd: () => { console.log('[NavDestinationTransition]', 'reached transitionOneFinish, empty now!') }
        };
        // 将transitionOne作为返回值传递给系统,系统侧将发起transitionOne对应的动画
        return [ transitionOne ];
      })
  }
}

3、动画效果演示

上面章节展示的自定义动画均如下图所示

(动图待补)

4、示例源码

(链接待补)

(未完待续……)


更多关于HarmonyOS鸿蒙Next中ArkUI路由/导航系列七:Navigation自定义转场动画,让页面切换炫起来的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

在HarmonyOS鸿蒙Next中,ArkUI的Navigation组件支持自定义转场动画。通过PageTransitionEnterPageTransitionExit接口,开发者可以定义页面进入和退出时的动画效果。使用动画属性(如translate、opacity、scale)配合curve曲线函数,可实现平滑过渡效果。需在pageTransition函数中配置动画参数,系统会自动应用于Navigation的页面切换。支持共享元素转场,通过matchedContent属性关联不同页面的组件。动画时长建议控制在300-500毫秒。

更多关于HarmonyOS鸿蒙Next中ArkUI路由/导航系列七:Navigation自定义转场动画,让页面切换炫起来的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


HarmonyOS Next Navigation自定义转场动画解析

从技术实现来看,HarmonyOS Next提供了两种级别的自定义转场动画控制方式:

1. Navigation级别全局动画

通过customNavContentTransition属性实现,主要特点:

  • 适用于整个Navigation容器内所有页面切换
  • 通过NavigationAnimatedTransition接口定义动画规则
  • 需要配合NavigationTransitionProxy控制动画生命周期
  • 示例中实现了上下滑动效果(PUSH时新页面从底部进入,POP时旧页面滑到底部)

关键点:

  • 使用CustomTransition工具类管理各页面动画状态
  • transition回调中区分PUSH/POP操作
  • 必须调用finishTransition()结束动画

2. NavDestination级别单页动画

通过customTransition属性实现,主要特点:

  • 仅控制当前页面的转场效果
  • 通过NavDestinationTransitionDelegate定义动画
  • 更简单直接,无需全局管理

关键点:

  • 根据NavigationOperationisEnter判断动画方向
  • 通过修改组件状态(如translateY)实现动画效果
  • 返回NavDestinationTransition数组定义具体动画参数

两种方式各有适用场景:全局动画适合统一风格,单页动画适合特殊需求。开发者可根据实际需求选择实现方式。

回到顶部