# 自定义路由
小程序采用多 WebView
架构,页面间跳转形式十分单一,仅能从右到左进行动画。而原生 App
的动画形式则多种多样,如从底部弹起,下一个为半屏页面,前一个页面下沉等。
Skyline
渲染引擎下,页面有两种渲染模式: WebView
和 Skyline
,它们通过页面配置中的 renderer
字段进行区分。在连续的 Skyline
页面间跳转时,可实现自定义路由效果,路由动画的曲线、时长均可交由开发者控制。
# 立即体验
扫码打开小程序示例,交互动画 - 基础组件 - 自定义路由 即可体验。
# 使用步骤
使用自定义路由分为 3 个步骤:
- 定义路由动画
- 页面跳转时指定路由类型
- 绑定页面手势
# 步骤一:定义路由动画
定义路由动画的主要步骤是注册一个 builder
函数,来定义小程序页面间切换过程的动画过程。
builder
函数接受的参数均为 sharedValue
类型,当路由事件发生时,Skyline
引擎会同步修改它们的值。开发者在 builder
函数中需要实现的是 handlePrimaryAnimation
和 handleSecondaryAnimation
这两个动画函数,当使用的 sharedValue
变化时,对应的 handlePrimaryAnimation
/ handleSecondaryAnimation
函数就会被执行。
// 注册 builder 函数
wx.router.addRouteBuilder("Cupertino", CupertinoRouteBuilder)
// 实现 builder 函数
const CupertinoRouteBuilder = ({
primaryAnimation,
secondaryAnimation,
primaryAnimationStatus,
secondaryAnimationStatus,
userGestureInProgress
}) => {
// 实现 worklet 动画函数 handlePrimaryAnimation 、 handleSecondaryAnimation
const { windowWidth } = wx.getWindowInfo();
const handlePrimaryAnimation = () => {
"worklet";
// primaryAnimation 是 builder 函数的入参
const translateX = windowWidth * (1 - primaryAnimation.value)
return {
transform: `translateX(${translateX}px)`
}
}
const handleSecondaryAnimation = ...
return {
handlePrimaryAnimation,
handleSecondaryAnimation,
opaque: true,
transitionDuration: 300,
reverseTransitionDuration: 300,
barrierColor: "",
barrierDismissible: false,
barrierLabel: "",
canTransitionTo: true,
canTransitionFrom: true,
}
}
# builder 函数
# 入参
属性 | 类型 | 说明 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
primaryAnimation | sharedValue | 页面进入和退出时的动画进度,取值:0 - 1 | ||||||||||||
secondaryAnimation | sharedValue | 下一个页面进入时,当前页面的动画进度,0-1 | ||||||||||||
primaryAnimationStatus | AnimationStatus: sharedValue | primaryAnimation 的状态 | ||||||||||||
| ||||||||||||||
secondaryAnimationStatus | AnimationStatus: sharedValue | secondaryAnimation 的状态 | ||||||||||||
userGestureInProgress | sharedValue | 手势进行中状态 |
# 出参
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
handlePrimaryAnimation | worklet 动画函数 | 返回 AnimatedStyle,控制页面的进入和退出动画 | |
handleSecondaryAnimation | worklet 动画函数 | 返回 AnimatedStyle,控制下一个页面进入时,当前页面的动画 | |
opaque | boolean | true | 页面推入后,原栈顶页面是否可见,true:不可见,false:可见 |
transitionDuration | number | 300 | 页面推入动画时长 |
reverseTransitionDuration | number | 300 | 页面退出动画时长 |
barrierColor | string | 页面推入时的遮罩层背景色,默认无 | |
barrierDismissible | boolean | false | 点击遮罩是否返回上一页 |
barrierLabel | string | 无障碍语义 | |
canTransitionTo | boolean | true | 下一个页面推入时,当前页 secondaryAnimation 是否生效 |
canTransitionFrom | boolean | true | 当前页推入时,前一个页面 secondaryAnimation 是否生效 |
# worklet 动画函数
builder
函数中的 handlePrimaryAnimation
和 handleSecondaryAnimation
都是 worklet
动画函数,返回 AnimatedStyle
,在路由过程中在 UI 线程执行,不会阻塞 JS 线程。
primaryAnimation
默认是线性的,为了丰富页面跳转的动画效果,worklet 支持了常见的动画缓动函数 Easing,开发者可以根据业务需求实现需要的页面切换动画效果。
/*
* CurveAnimation 利用 derivedValue 机制
* 封装了进入和退出时的动画曲线
*/
function CurveAnimation ({ animation, animationStatus, curve, reverseCurve }) {
const { derived } = wx.worklet;
return derived(() => {
'worklet';
const useForwardCurve = !reverseCurve || animationStatus.value !== AnimationStatus.reverse;
const activeCurve = useForwardCurve ? curve : reverseCurve;
const t = animation.value;
if (!activeCurve) return t;
if (t === 0 || t === 1) return t;
return activeCurve(t);
});
}
/**
* worklet 支持了常见的动画缓动函数
* 示例使用 bezier 三次贝塞尔曲线
*/
const Curves = {};
if (wx.worklet) {
const { Easing } = wx.worklet;
Object.assign(Curves, {
fastLinearToSlowEaseIn: Easing.cubicBezier(0.18, 1.0, 0.04, 1.0),
linearToEaseOut: Easing.cubicBezier(0.35, 0.91, 0.33, 0.97),
easeInToLinear: Easing.cubicBezier(0.67, 0.03, 0.65, 0.09),
fastOutSlowIn: Easing.cubicBezier(0.4, 0.0, 0.2, 1.0),
});
}
const { windowWidth } = wx.getWindowInfo();
const _curvePrimaryAnimation = CurveAnimation({
animation: primaryAnimation, // builder 函数入参
animationStatus: primaryAnimationStatus, // builder 函数入参
curve: Curves.linearToEaseOut,
reverseCurve: Curves.easeInToLinear,
});
/**
* primaryAnimation 控制页面进入和退出动画
* 1. 页面进入时采用 curve 曲线生成的值
* 2. 页面退出时采用 reverseCurve 生成的值
*/
const handlePrimaryAnimation = () => {
"worklet";
let t = primaryAnimation.value;
if (!userGestureInProgress.value) {
t = _curvePrimaryAnimation.value;
}
const translateX = windowWidth * (1 - t)
return {
transform: `translateX(${translateX}px)`
};
};
# 步骤二:页面跳转时指定路由类型
// 低版本基础库会降级到多 webview 下的路由动画
wx.navigateTo({
url: 'pageB',
routeType: 'Cupertino',
})
# 步骤三:绑定页面手势
通常我们会在页面上绑定手势,除了用户点击按钮返回外,还可以向右、向下拖动页面返回。此时就需要在页面内绑定手势进行处理。
绑定手势之后,根据 GestureState
状态值来接管页面动画,手势事件 this.customRouteContext
包含当前页面定义路由 builder 时的全部变量。
在手势事件中根据手势来修改 primaryAnimation
的值实现页面手势动画。
# GestureState 状态值
值 | 说明 |
---|---|
0 | POSSIBLE 此时手势未识别,如 panDown等 |
1 | BEGIN 手势已识别 |
2 | ACTIVE 连续手势活跃状态 |
3 | END 手势终止 |
4 | CANCELLED 手势取消 |
# this.customRouteContext 属性
手势接管开始必需调用 startUserGesture,手势结束必须调用 stopUserGesture,退出页面必须调用 didPop。
Skyline 引擎无法内部处理掉这些调用,原因是引擎无法得知开发者最终是否想退出这个页面。
属性 | 类型 | 说明 |
---|---|---|
didPop | function | 退出页面时调用事件 |
startUserGesture | function | 开始页面手势事件,更新 userGesturelnProgress 值为 1 |
stopUserGesture | function | 结束页面手势事件,更新 userGesturelnProgress 值为 0 |
skylinePageld | sharedValue | |
secondaryAnimation | sharedValue | |
userGesturelnProgress | sharedValue | |
primaryAnimationStatus | sharedValue | |
primaryAnimation | sharedValue | |
skylineWindowld | sharedValue | |
secondaryAnimationStatus | sharedValue | |
routeType | string |
# 示例代码
<!-- 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);
}
}
})
# 原理说明
当前页面栈顶为 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
表示当前是否处于手势返回过程中。默认路由效果下,手势拖动页面返回时,页面动画会是一个线性的,跟随手移动的状态。而调用接口返回时,则是不同的曲线。
# 开箱即用自定义路由代码
这个包装到代码片段或者github直接供开发者使用