Worklet 动画
微信小程序是双线程的,分为 UI 线程和 JS 线程,两者独立运行,不会相互阻塞
既然相互独立,那沟通就会有延迟,以以下代码为例:
<view
class="wrapper"
bindtap="onChangeOpacity"
style="opacity: {{ opacity }}"
></view>
Component({
data: {
opacity: 1,
},
methods: {
onChangeOpacity() {
this.setData({
opacity: this.data.opacity ? 0 : 1,
})
},
},
})
.wrapper {
width: 200px;
height: 200px;
background: red;
transition: opacity 1.5s;
}
通过点击背景让其颜色由浅至深、由深至浅。执行顺序为:
[UI 线程] tap 事件,通知
-> [JS 线程] 执行 onChangeOpacity 回调;检测到opacity
变化,通知
-> [UI]线程 绘制(应用动画)
上述场景这种方式并没什么不妥,不过一旦涉及到手势交互、频发触发等场景;这种双向沟通机制就会存在延迟、不确定性
因此,就需要一种可以运行在 UI 线程的逻辑,这样就不需要 UI 和 JS 两个线程之间反复横跳了
worklet 就应运而生,它不仅可以在 UI 线程运行,也可以在 JS 线程运行
核心概念
const { shared } = wx.worklet
shared
定义 UI 和 JS 线程的共享变量,可以类比vue3
的ref
,访问也是通过.value
(定义的变量假如不是
shared
类型,worklet
函数如果有访问它,会在一开始序列化后生成在 UI 线程的拷贝,后续 JS 线程对它的更新都不会同步至 UI 线程)applyAnimatedStyle(selector, updater[, userConfig, callback])
通过页面/组件实例访问,该函数通过
selector
选中对应元素,updater
(worklet
函数)返回一个styleObject
,这个styleObject
中的value
与sharedValue
绑定。当sharedValue
变化时,触发updater
执行,元素就有动画了
<pan-gesture-handler worklet:ongesture="onPan">
<view class="circle"></view>
</pan-gesture-handler>
const { shared } = wx.worklet
const offset = shared(0)
const GestureStateEnum {
// 手势未识别
POSSIBLE: 0,
// 手势已识别
BEGIN: 1,
// 连续手势活跃状态
ACTIVE: 2,
// 手势终止
END: 3,
// 手势取消
CANCELLED: 4,
}
Component({
lifetimes: {
ready() {
this.applyAnimatedStyle('.circle', () => {
'worklet'
return {
transform: `translateX(${offset.value}px)`,
}
})
},
},
methods: {
onPan(evt) {
'worklet'
if (evt.state === GestureStateEnum.ACTIVE) {
offset.value += evt.deltaX
}
},
},
})
动画函数:
const { timing, Easing, spring, decay } = wx.worklet
动画函数都是第一个参数接收目标值
toValue
,后续参数(可选的)接收配置。返回值赋值给sharedValue
,由此便完成了动画的闭环spring
:基于物理的动画decay
:基于滚动衰减的动画
案例
案例一
实现:
- 上部分:image + 下部分:scroll-view 布局
- 下拉时,图片和 scroll-view 同步下移(可以有个最大下移距离),松手回弹
- 上滑时,scroll-view 容器先上移至与系统状态栏对齐,再进行内部的滚动
步骤 1:实现下拉回弹
初步实现
<image
class="banner absolute w-full"
src="https://images.pexels.com/photos/2912996/pexels-photo-2912996.jpeg?auto=compress&cs=tinysrgb&w=800"
fade-in
></image>
<view class="flex-1">
<!-- 该手势组件先通过native-view="scroll-view"代理该标签,再通过onShouldAcceptGesture和onShouldAcceptGesture的逻辑来控制scroll-view层上的手势是否有效,控制内部滚动 -->
<vertical-drag-gesture-handler
native-view="scroll-view"
worklet:ongesture="onDragScrollView"
should-response-on-move="onShouldResponseOnMove"
should-accept-gesture="onShouldAcceptGesture"
>
<scroll-view
class="h-full"
scroll-y
>
...
</scroll-view>
</vertical-drag-gesture-handler>
</view>
const { shared, timing } = wx.worklet
const imageHeight = shared(0)
const GestureStateEnum {
// 手势未识别
POSSIBLE: 0,
// 手势已识别
BEGIN: 1,
// 连续手势活跃状态
ACTIVE: 2,
// 手势终止
END: 3,
// 手势取消
CANCELLED: 4,
}
Component({
data: {
initImageHeight: 474,
},
lifetimes: {
ready() {
imageHeight.value = this.data.initImageHeight
this.applyAnimatedStyle('.banner', () => {
'worklet'
return {
height: `${imageHeight.value}rpx`,
}
})
},
},
methods: {
onDragScrollView(evt) {
'worklet'
if (evt.state === GestureStateEnum.ACTIVE) {
if (evt.deltaY > 0) {
// 下拉,图片高度增加,scroll-view的bounces下拉效果
imageHeight.value += evt.deltaY
} else {
// 上滑
}
} else if ([GestureStateEnum.END, GestureStateEnum.CANCELLED].includes(evt.state)) {
// 手势结束,重置状态
imageHeight.value = timing(this.data.initImageHeight)
}
},
onShouldResponseOnMove() {
'worklet'
return true
},
onShouldAcceptGesture() {
'worklet'
return true
},
},
})
skyline 下,scroll-view
的bounces
效果在Android
和iOS
均生效,即:向下拉动,顶部会跟随展示背景色,松手回弹。那一开始将image
设置成absolute
,然后在worklet:ongesture
中改变image
的height
,即可实现下拉,上下部分同步下移。但是:回弹时,scroll-view
的回弹动画和时间不得而知,所以两者的回弹效果始终是对不齐的
因此,需要关闭bounces
效果,下半部分也通过动画以实现上下运动均同步,以下是改进后的代码:
改进实现
<image
class="banner absolute w-full"
src="https://images.pexels.com/photos/2912996/pexels-photo-2912996.jpeg?auto=compress&cs=tinysrgb&w=800"
fade-in
></image>
<view class="sv flex-1">
<!-- 该手势组件先通过native-view="scroll-view"代理该标签,再通过onShouldAcceptGesture和onShouldAcceptGesture的逻辑来控制scroll-view层上的手势是否有效,控制内部滚动 -->
<vertical-drag-gesture-handler
native-view="scroll-view"
worklet:ongesture="onDragScrollView"
should-response-on-move="onShouldResponseOnMove"
should-accept-gesture="onShouldAcceptGesture"
>
<scroll-view
class="h-full"
scroll-y
bounces="{{ false }}">
...
</scroll-view>
</vertical-drag-gesture-handler>
</view>
const { shared, timing } = wx.worklet
const imageHeight = shared(0)
const scrollViewTranslateY = shared(0)
const GestureStateEnum {
// 手势未识别
POSSIBLE: 0,
// 手势已识别
BEGIN: 1,
// 连续手势活跃状态
ACTIVE: 2,
// 手势终止
END: 3,
// 手势取消
CANCELLED: 4,
}
Component({
data: {
initImageHeight: 474,
},
lifetimes: {
ready() {
scrollViewTranslateY.value = imageHeight.value = this.data.initImageHeight
this.applyAnimatedStyle('.banner', () => {
'worklet'
return {
height: `${imageHeight.value}rpx`,
}
})
this.applyAnimatedStyle('.sv', () => {
'worklet'
return {
transform: `translateY(${scrollViewTranslateY.value}rpx)`,
}
})
},
},
methods: {
onDragScrollView(evt) {
'worklet'
if (evt.state === GestureStateEnum.ACTIVE) {
if (evt.deltaY > 0) {
// 下拉,图片高度增加,scroll-view向下平移
imageHeight.value += evt.deltaY
scrollViewTranslateY.value += evt.deltaY
} else {
// 上滑
}
} else if ([GestureStateEnum.END, GestureStateEnum.CANCELLED].includes(evt.state)) {
// 手势结束,重置状态
scrollViewTranslateY.value = imageHeight.value = timing(this.data.initImageHeight)
}
},
onShouldResponseOnMove() {
'worklet'
return true
},
onShouldAcceptGesture() {
'worklet'
return true
},
},
})
步骤 2:上滑时,容器先上移,再内部滚动
如果手势向上滑动时,scroll-view
是向上平移还是内部滚动呢?该怎么写逻辑呢?
前面部分已经知道,vertical-drag-gesture-handler
手势组件的should-accept-gesture
和should-response-on-move
回调可以控制当前手势有没有效,如果返回true
就是有效,返回false
就是无效
向不向上平移还是内部滚动其实都依赖一个关键点,就是:scroll-view
的tranlateY
当前是 在起始位置 还是 到达了顶部位置
已知:translateY
起始位置是initImageHeight
,我们定义一个顶部位置为topDestination
因此要控制scroll-view
能不能滚动,可以在should-response-on-move
中返回:evt.deltaY < 0 && scrollViewTranslateY.value === topDestination
此时问题就来了:如果should-response-on-move
这么判定,初始时,scrollViewTranslateY.value
为initImageHeight
,向上滑动时,判定为false
,永远也触发不了worklet:ongesture
的回调,那谁来改变scrollViewTranslateY.value
值到topDestination
呢?
答案:手势协商,通过两个vertical-drag-gesture-handler
组件,外围控制纵向拖动,内侧控制内部滚动。再用simultaneous-handlers
声明两个手势可同时触发
<image
class="banner absolute w-full"
src="https://images.pexels.com/photos/2912996/pexels-photo-2912996.jpeg?auto=compress&cs=tinysrgb&w=800"
fade-in
></image>
<view class="sv flex-1">
<!-- 该手势组件控制向上拖动 -->
<vertical-drag-gesture-handler
worklet:ongesture="onDragScrollViewOuter"
tag="outer"
simultaneous-handlers="{{['inner']}}"
>
<!-- 该手势组件先通过native-view="scroll-view"代理该标签,再通过onShouldAcceptGesture和onShouldAcceptGesture的逻辑来控制scroll-view层上的手势是否有效,控制内部滚动 -->
<vertical-drag-gesture-handler
tag="inner"
native-view="scroll-view"
worklet:ongesture="onDragScrollViewInner"
should-accept-gesture="onShouldAcceptGestureInner"
simultaneous-handlers="{{['outer']}}"
>
<scroll-view
class="h-full"
scroll-y
bounces="{{ false }}"
>
...
</scroll-view>
</vertical-drag-gesture-handler>
</vertical-drag-gesture-handler>
</view>
const { shared, timing } = wx.worklet
const imageHeight = shared(0)
const scrollViewTranslateY = shared(0)
const GestureStateEnum {
// 手势未识别
POSSIBLE: 0,
// 手势已识别
BEGIN: 1,
// 连续手势活跃状态
ACTIVE: 2,
// 手势终止
END: 3,
// 手势取消
CANCELLED: 4,
}
Component({
data: {
initImageHeight: 474,
},
lifetimes: {
ready() {
scrollViewTranslateY.value = imageHeight.value = this.data.initImageHeight
this.applyAnimatedStyle('.banner', () => {
'worklet'
return {
height: `${imageHeight.value}rpx`,
}
})
this.applyAnimatedStyle('.sv', () => {
'worklet'
return {
transform: `translateY(${scrollViewTranslateY.value}rpx)`,
}
})
},
},
methods: {
onDragScrollView(evt) {
'worklet'
if (evt.state === GestureStateEnum.ACTIVE) {
if (evt.deltaY > 0) {
// 下拉,图片高度增加,scroll-view向下平移
imageHeight.value += evt.deltaY
scrollViewTranslateY.value += evt.deltaY
} else {
// 上滑
}
} else if ([GestureStateEnum.END, GestureStateEnum.CANCELLED].includes(evt.state)) {
// 手势结束,重置状态
scrollViewTranslateY.value = imageHeight.value = timing(this.data.initImageHeight)
}
},
onShouldAcceptGestureInner() {
'worklet'
return true
},
},
})