自定义转场动画

HMRouter支持自定义转场动画,开发者可以通过实现IHMAnimator接口来定义页面切换效果,实现丰富多样的转场体验。

接口定义

自定义转场动画需要实现IHMAnimator接口:

interface IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void;
  interactive?(handle: HMAnimatorHandle): void;
}

原理介绍

开发者通过实现IHMAnimator接口,覆盖effectinteractive方法,来对动画效果进行自定义。

动画效果处理器HMAnimatorHandle在页面加载aboutToAppear时进行初始化,对动画效果进行预设,并在页面跳转时进行调用。

一次性动画的处理器会在页面跳转时进行重新覆盖。

因此需注意:

  • 全局动画的变更不会影响到已经加载的页面(包括单例页面),只有页面重新加载才会生效

动画状态设置

effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle)方法中提供了入场动画enterHandle和出场动画exitHandle2组动画状态设置。

通过对HMAnimatorHandle回调中的页面位移/缩放/透明度等状态进行设置,来实现动画效果:

  • 入场动画分为主动入场enterHandle.start/finish/onFinish和被动入场enterHandle.passiveStart/passiveFinish/passiveOnFinish
  • 出场动画分为主动出场exitHandle.start/finish/onFinish和被动出场exitHandle.passiveStart/passiveFinish/passiveOnFinish

每种动画可以设置3种页面状态:

  • 动画开始状态start
  • 动画完成状态finish
  • 动画结束状态onFinish

通过设置动画曲线让页面在开始和完成状态之间进行过渡,默认动画曲线为Curve.EaseIn,结束状态用于将页面还原,释放资源等。

页面跳转动画流程

A页面push/replace到B页面:B页面为主动入场,A页面为被动出场

push动画流程

B页面pop到A页面:B页面为主动出场,A页面为被动入场

pop动画流程

动画曲线、持续时间、超时时间等也需要通过HMAnimatorHandle进行设置。

HMAnimatorHandle也提供了自定义动画回调customAnimationpassiveCustomAnimation让开发者对动画进行完全控制。

自定义动画类型

  • 全局自定义动画:通过HMNavigation初始化时定义,并且可以通过HMRouterMgr进行修改
  • 页面自定义动画:通过@HMRouter标签进行定义,页面跳转时分别执行2个页面定义的动画,如果有页面未定义则使用全局动画
  • 一次性动画:通过HMRouterMgr进行页面跳转时定义,同时执行一次性动画中的入场动画和出场动画

一次性动画的覆盖原则

  • 路由push/replace时传入一次性动画,目标页面执行一次性动画的主动入场,源页面执行一次性动画的被动离场
  • push/replace时传入的一次性动画根据目标页面生存周期,当目标页面pop时未传入一次性动画,则执行push/replace时的一次性动画,pop目标页面执行一次性动画的被动入场,源页面执行主动离场
  • 当pop时传入一次性动画,目标页面的被动入场和源页面的主动离场均查找一次性动画
  • 上述场景描述的主动入场,被动入场,主动离场,被动离场动画,如果一次性动画内未定义,则会执行页面定义的动画或者全局动画定义

交互式事件配置

interactive?(handle: HMAnimatorHandle)方法中提供了交互式事件配置。

通过对HMAnimatorHandle回调中的手势事件回调来进行路由操作,实现动画状态的跟随手势变化:

  • actionStart: 手势开始事件,在事件中触发路由跳转,通过对手势位移判断来决定页面是push/replace还是pop
  • updateProgress: 手势更新事件,在事件中通过设置转场进度百分比来触发动画曲线的跟随手势变化
  • actionEnd: 手势结束事件,在事件中对手势位移判断来调用NavigationTransitionProxy决定转场状态是完成转场还是取消转场

交互式转场方法interactive不设置动画效果,只是通过手势事件对转场行为进行处理并控制动画曲线的进度,动画效果需要配合effect方法进行定义。

实现示例

基本转场动画

import { HMAnimator, HMAnimatorHandle, IHMAnimator } from "@hadss/hmrouter";
import { AttributeUpdater } from "@kit.ArkUI";

@HMAnimator({ animatorName: 'FadeAnimator' })
export class FadeAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 入场动画
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(0);
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(1);
    }).onFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(1);
    });

    enterHandle.duration = 300;
    enterHandle.curve = Curve.EaseOut;

    // 出场动画
    exitHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(1);
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(0);
    }).onFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.opacity(1);
    });

    exitHandle.duration = 300;
    exitHandle.curve = Curve.EaseIn;
  }
}

滑动转场动画

import { HMAnimator, HMAnimatorHandle, IHMAnimator } from "@hadss/hmrouter";
import { AttributeUpdater } from "@kit.ArkUI";

@HMAnimator({ animatorName: 'SlideAnimator' })
export class SlideAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 主动入场动画(push时目标页面)
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "100%" });
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    }).onFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    });

    enterHandle.duration = 300;
    enterHandle.curve = Curve.EaseOut;

    // 被动出场动画(push时源页面)
    exitHandle.passiveStart((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    }).passiveFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "-30%" });
    }).passiveOnFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    });

    exitHandle.duration = 300;
    exitHandle.curve = Curve.EaseOut;

    // 主动出场动画(pop时源页面)
    exitHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "100%" });
    }).onFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    });

    // 被动入场动画(pop时目标页面)
    enterHandle.passiveStart((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "-30%" });
    }).passiveFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    }).passiveOnFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    });
  }
}

交互式转场动画

import { HMAnimator, HMAnimatorHandle, IHMAnimator } from "@hadss/hmrouter";
import { AttributeUpdater } from "@kit.ArkUI";

@HMAnimator({ animatorName: 'InteractiveAnimator' })
export class InteractiveAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 定义与上面SlideAnimator相同的动画效果
    // 主动入场动画(push时目标页面)
    enterHandle.start((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "100%" });
    }).finish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    }).onFinish((modifier: AttributeUpdater<NavDestinationAttribute>) => {
      modifier.attribute?.translate({ x: "0" });
    });

    // 其余动画设置...
  }

  interactive(handle: HMAnimatorHandle): void {
    // 手势开始事件
    handle.actionStart((event: GestureEvent, proxy: NavigationTransitionProxy, operation: string, startOffset?: { width: number, height: number }) => {
      if (!startOffset) {
        return;
      }

      // 根据手势方向判断是push还是pop
      if (event.offsetX > 0) {
        // 向右滑动,执行pop操作
        proxy.pop();
      } else if (event.offsetX < 0 && operation === 'push') {
        // 向左滑动,执行push操作
        proxy.push();
      }
    });

    // 手势更新事件
    handle.updateProgress((event: GestureEvent, progress: { value: number }, startOffset?: { width: number, height: number }) => {
      if (!startOffset) {
        return;
      }

      // 计算进度百分比
      let percent = Math.abs(event.offsetX) / startOffset.width;
      percent = Math.min(1, percent);

      // 设置动画进度
      progress.value = percent;
    });

    // 手势结束事件
    handle.actionEnd((event: GestureEvent, proxy: NavigationTransitionProxy, operation: string, startOffset?: { width: number, height: number }) => {
      if (!startOffset) {
        return;
      }

      // 判断是完成转场还是取消转场
      let percent = Math.abs(event.offsetX) / startOffset.width;
      if (percent > 0.3) {
        // 超过30%,完成转场
        proxy.finish();
      } else {
        // 未超过30%,取消转场
        proxy.cancel();
      }
    });
  }
}

自定义复杂动画

import { HMAnimator, HMAnimatorHandle, IHMAnimator } from "@hadss/hmrouter";
import { AttributeUpdater } from "@kit.ArkUI";

@HMAnimator({ animatorName: 'CustomComplexAnimator' })
export class CustomComplexAnimator implements IHMAnimator {
  effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
    // 使用customAnimation完全控制动画
    enterHandle.customAnimation((progress: number, modifier: AttributeUpdater<NavDestinationAttribute>) => {
      // 根据进度计算动画参数
      modifier.attribute?.translate({ y: `${(1 - progress) * 100}%` })
        .scale({ x: 0.8 + 0.2 * progress, y: 0.8 + 0.2 * progress })
        .opacity(progress);
    });

    exitHandle.passiveCustomAnimation((progress: number, modifier: AttributeUpdater<NavDestinationAttribute>) => {
      // 根据进度计算动画参数
      modifier.attribute?.scale({ x: 1 - 0.1 * progress, y: 1 - 0.1 * progress })
        .opacity(1 - 0.5 * progress);
    });

    enterHandle.duration = 400;
    enterHandle.curve = Curve.Friction;
  }
}

使用自定义动画

全局动画设置

// 在HMNavigation中设置
HMNavigation({
  navigationId: 'mainNavigation',
  homePageUrl: 'HomePage',
  options: {
    standardAnimator: 'SlideAnimator',
    dialogAnimator: 'FadeAnimator'
  }
})

// 或者通过HMRouterMgr设置
HMRouterMgr.setGlobalStandardAnimator('SlideAnimator', 'mainNavigation');
HMRouterMgr.setGlobalDialogAnimator('FadeAnimator', 'mainNavigation');

页面级动画设置

@HMRouter({
  pageUrl: 'DetailPage',
  animator: 'SlideAnimator'
})
@Component
export struct DetailPage {
  // ...
}

跳转时指定动画

// 使用动画名称
HMRouterMgr.push({
  pageUrl: 'DetailPage',
  animator: 'SlideAnimator'
});

// 使用动画实例
const animator = new SlideAnimator();
HMRouterMgr.push({
  pageUrl: 'DetailPage',
  animator: animator
});

内置动画

HMRouter提供了一些内置的动画效果,可以直接使用:

标准页面动画

// 使用内置标准动画
HMNavigation({
  navigationId: 'mainNavigation',
  homePageUrl: 'HomePage',
  options: {
    standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR
  }
})

对话框动画

// 使用内置对话框动画
HMNavigation({
  navigationId: 'mainNavigation',
  homePageUrl: 'HomePage',
  options: {
    dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR
  }
})

注意事项

  1. 动画效果应该考虑双向性,即push和pop的动画效果应该是对称的
  2. 动画持续时间不宜过长,一般建议在300-500ms之间
  3. 交互式动画需要考虑手势取消的情况,确保用户体验的连贯性
  4. 复杂动画可能会影响性能,应进行充分测试
  5. 全局动画变更不会影响已加载的页面,只有页面重新加载才会生效