# 自定义路由

小程序采用多 WebView 架构,页面间跳转形式十分单一,仅能从右到左进行动画。而原生 App 的动画形式则多种多样,如从底部弹起,下一个为半屏页面,前一个页面下沉等。

Skyline 渲染引擎下,页面有两种渲染模式: WebViewSkyline,它们通过页面配置中的 renderer 字段进行区分。在连续的 Skyline 页面间跳转时,可实现自定义路由效果,路由动画的曲线、时长均可交由开发者控制。

# 立即体验

# 使用方法

使用自定义路由前,我们需要先注册一个路由的 builder 函数。

下面的示例完整的实现了 iOS 上的默认路由效果,我们可以从中学习如何控制自定义路由动画。

# 步骤一:定义路由动画

const { windowWidth } = wx.getWindowInfo();

export const CupertinoRouteBuilder = ({
  primaryAnimation,
  secondaryAnimation,
  primaryAnimationStatus,
  secondaryAnimationStatus,
  userGestureInProgress,
}: IRouteContext) => {
  // CurveAnimation 利用 derivedValue 机制
  // 封装了进入和退出时的动画曲线
  const _curvePrimaryAnimation = CurveAnimation({
    animation: primaryAnimation,
    animationStatus: primaryAnimationStatus,
    curve: Curves.linearToEaseOut,
    reverseCurve: Curves.easeInToLinear,
  });

  const handlePrimaryAnimation = () => {
    "worklet";
    /**
     * primaryAnimation 控制页面进入和退出动画
     * 1. 页面进入时采用 curve 曲线生成的值
     * 2. 页面退出时采用 reverseCurve 生成的值
     */
    let t = primaryAnimation.value;
    if (!userGestureInProgress.value) {
      t = _curvePrimaryAnimation.value;
    }

    const translateX = windowWidth * (1 - t)
    return {
      transform: `translateX(${translateX}px)`
    };
  };

  const _curveSecondaryAnimation = CurveAnimation({
    animation: secondaryAnimation,
    animationStatus: secondaryAnimationStatus,
    curve: Curves.linearToEaseOut,
    reverseCurve: Curves.easeInToLinear,
  });

  const handleSecondaryAnimation = () => {
    "worklet";
    /**
     * secondaryAnimation 控制下一个页面进入时,当前页动画
     */
    let t = secondaryAnimation.value;
    if (!userGestureInProgress.value) {
      t = _curveSecondaryAnimation.value;
    }

    // 下一个页面推入时,当前页面继续向左推入 1/3
    const translateX = (-1 / 3) * windowWidth * t 
    return {
      transform: `translateX(${translateX}px)`,
    };
  };
  
  return {
    // 控制页面的进入和退出动画
    handlePrimaryAnimation,

    // 控制下一个页面进入时,当前页面的动画
    handleSecondaryAnimation,
    
    // 下一个页面推入后,原栈顶页面是否可见,默认不可见
    opaque: true, 

    // 页面推入动画时长
    transitionDuration: 300, 

    // 页面退出动画时长
    reverseTransitionDuration: 300, 

    // 下一个页面推入时的遮罩层背景色,默认无
    barrierColor: "", 

    // 点击遮罩是否返回上一页
    barrierDismissible: false, 

    // 无障碍语义
    barrierLabel: "", 

    // 下一个页面推入时,当前页 secondaryAnimation 是否生效
    canTransitionTo: true, 

    // 当前页推入时,前一个页面 secondaryAnimation 是否生效
    canTransitionFrom: true, 
  };
};

# 步骤二:页面跳转时指定路由类型

// 页面跳转前首先需要注册路由 builder
wx.worklet.addRouteBuilder("Cupertino", CupertinoRouteBuilder);

// 低版本基础库会降级到多 webview 下的路由动画
wx.navigateTo({
  url: 'pageB',
  routeType: 'Cupertino'
})

# 步骤三:绑定页面手势

通常我们会在页面上绑定手势,除了用户点击按钮返回外,还可以向右、向下拖动页面返回。此时就需要在页面内绑定手势进行处理。

<!-- absolute 定位到页面左侧的返回条,在上面滑动时可以控制页面路由动画 -->
<horizontal-drag-gesture-handler onGestureEvent="handleHorizontalDrag">
  <view class="gesture-back-area"></view>
<horizontal-drag-gesture-handler/>
/**
 * 1. customRouteContext 中包含当前页面定义路由 builder 时的全部变量
 * 2. 可以从中获取 primaryAnimation,对其进行修改,进而驱动页面动画
 * 3. 手势返回时由开发者控制 primaryAnimation 的更新
 * 4. 接口调用页面进入和退出时,由 skyline 引擎控制 primaryAnimation 的更新
 */
Page({
  handleDragStart() {
    "worklet";
    const { startUserGesture } = this.customRouteContext;
    // 触摸开始时需要调用 startUserGesture
    // 更新 userGestureInProgress 的值
    // ⚠️⚠️ 
    startUserGesture();
  },

  handleDragUpdate(delta) {
    "worklet";
    const { primaryAnimation } = this.customRouteContext;
    const newVal = primaryAnimation.value - delta;
    // 跟随手指拖动页面效果
    primaryAnimation.value = clamp(newVal, 0.0, 1.0);
  },

  handleDragEnd(velocity) {
    "worklet";
    const { primaryAnimation, stopUserGesture, didPop } =
      this.customRouteContext;

    // 页面滑动超过一半或者手指离开时的释放速度大于指定值时
    // 判定最终要退出页面,继续滑动页面到右侧边缘
    // 否则判定取消此时退出,页面返回到左侧边缘
    let animateForward = false;
    if (Math.abs(velocity) >= _kMinFlingVelocity) {
      animateForward = velocity <= 0;
    } else {
      animateForward = primaryAnimation.value > 0.5;
    }
    const t = primaryAnimation.value;
    const animationCurve = Curves.fastLinearToSlowEaseIn;
    if (animateForward) {
      const droppedPageForwardAnimationTime = Math.min(
        Math.floor(lerp(_kMaxDroppedSwipePageForwardAnimationTime, 0, t)),
        _kMaxPageBackAnimationTime
      );

      // primaryAnimation.vaue = 1 表示未退出页面
      primaryAnimation.value = timing(
        1.0,
        {
          duration: droppedPageForwardAnimationTime,
          easing: animationCurve,
        },
        () => {
          // 最终均需调用 stopUserGesture
          // ⚠️⚠️ 
          stopUserGesture();
        }
      );
    } else {
      const droppedPageBackAnimationTime = Math.floor(
        lerp(0, _kMaxDroppedSwipePageForwardAnimationTime, t)
      );
      // primaryAnimation.vaue = 0 表示退出页面
      primaryAnimation.value = timing(
        0.0,
        {
          duration: droppedPageBackAnimationTime,
          easing: animationCurve,
        },
        () => {
          // 退出页面时需调用 didPop
          // ⚠️⚠️
          didPop();

          // 最终均需调用 stopUserGesture
          // ⚠️⚠️
          stopUserGesture();
        }
      );
    }
  },

  handleHorizontalDrag(gestureEvent) {
    "worklet";
    if (gestureEvent.state === GestureState.BEGIN) {
      this.handleDragStart();
    } else if (gestureEvent.state === GestureState.ACTIVE) {
      const delta = gestureEvent.deltaX / windowWidth;
      this.handleDragUpdate(delta);
    } else if (gestureEvent.state === GestureState.END) {
      const velocity = gestureEvent.velocityX / windowWidth;
      this.handleDragEnd(velocity);
    } else if (gestureEvent.state === GestureState.CANCELLED) {
      this.handleDragEnd(0.0);
    }
  }
})

标记处是任意类型页面路由手势绑定时,均需调用接口。

至于为什么 Skyline 引擎无法内部处理掉这些调用,原因是引擎无法得知开发者最终是否想退出这个页面。

以上代码看起来很长,但自定义路由的代码基本都是这个模板,理解了这一种方式,就可以定制任意你想要的效果。

# 原理说明

route

当前页面栈顶为 PageA,当调用 wx.navigateTo 推入 PageB 时:

  • PageA 对应的 secondaryAnimation0.0 -> 1.0PageBprimaryAnimation0.0 -> 1.0

此时页面栈顶变为 PageB,当调用 wx.navigateBack 推出 PageB时:

  • PageA 对应的 secondaryAnimation1.0 -> 0.0PageBprimaryAnimation1.0 -> 0.0

primaryAnimationsecondaryAnimation 均为 SharedValue 类型(如果你对如何使用 SharedValue 还不熟悉,可以先看看 worklet 文档),表示路由的进度。当路由事件发生时,Skyline 引擎会同步修改它们的值。

handlePrimaryAnimationhandleSecondaryAnimation 返回 AnimatedStyle,它们的作用跟 this.applyAnimaedStyle 接口是一样,只不过作用于页面的 “根” 节点。

userGestureInProgress 表示当前是否处于手势返回过程中。默认路由效果下,手势拖动页面返回时,页面动画会是一个线性的,跟随手移动的状态。而调用接口返回时,则是不同的曲线,