# 自定义路由
小程序采用多 WebView
架构,页面间跳转形式十分单一,仅能从右到左进行动画。而原生 App
的动画形式则多种多样,如从底部弹起,下一个为半屏页面,前一个页面下沉等。
Skyline
渲染引擎下,页面有两种渲染模式: WebView
和 Skyline
,它们通过页面配置中的 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
引擎无法内部处理掉这些调用,原因是引擎无法得知开发者最终是否想退出这个页面。
以上代码看起来很长,但自定义路由的代码基本都是这个模板,理解了这一种方式,就可以定制任意你想要的效果。
# 原理说明
当前页面栈顶为 PageA
,当调用 wx.navigateTo
推入 PageB
时:
PageA
对应的secondaryAnimation
从0.0 -> 1.0
,PageB
的primaryAnimation
从0.0 -> 1.0
此时页面栈顶变为 PageB
,当调用 wx.navigateBack
推出 PageB
时:
PageA
对应的secondaryAnimation
从1.0 -> 0.0
,PageB
的primaryAnimation
从1.0 -> 0.0
primaryAnimation
和 secondaryAnimation
均为 SharedValue
类型(如果你对如何使用 SharedValue
还不熟悉,可以先看看 worklet 文档),表示路由的进度。当路由事件发生时,Skyline
引擎会同步修改它们的值。
handlePrimaryAnimation
和 handleSecondaryAnimation
返回 AnimatedStyle
,它们的作用跟 this.applyAnimaedStyle
接口是一样,只不过作用于页面的 “根” 节点。
userGestureInProgress
表示当前是否处于手势返回过程中。默认路由效果下,手势拖动页面返回时,页面动画会是一个线性的,跟随手移动的状态。而调用接口返回时,则是不同的曲线,