仅在微信小程序测试,多端请自行添加条件编译
首先是一个 worklet.ts 的工具
declare const wx: {
worklet: {
timing: (value: number, options?: { duration?: number, easing?: any }, callback?: Function) => AnimationObject
runOnJS: any
shared: <T>(val: T) => SharedValue<T>
derived: <T>(val: () => T) => SharedValue<T>
Easing: {
in: any
out: any
inOut: any
ease: any
}
}
}
export interface SharedValue<T = any> { value: T }
export type AnimationObject = any
export const worklet = wx.worklet
export function getMpInstance() {
return (getCurrentInstance()?.proxy as any).$scope
}
export interface MpInstance {
applyAnimatedStyle: (selector: string, callback: () => Record<string, any>) => void
}
export function workletValue<T>(val: T) {
return worklet.shared(val)
}
export function workletDerived<T>(val: () => T) {
return worklet.derived(val)
}
export function workletMethods<T extends { [key: string]: Function }>(methods: T) {
const mpInstance = getMpInstance()
for (const key in methods)
mpInstance[key] = methods[key]
return methods
}
export function useApplyAnimatedStyle() {
const mpInstance = getMpInstance()
return (selector: string, callback: () => Record<string, any>) => {
mpInstance.applyAnimatedStyle(selector, callback)
}
}
export const GestureState = {
POSSIBLE: 0, // 0 此时手势未识别,如 panDown等
BEGIN: 1, // 1 手势已识别
ACTIVE: 2, // 2 连续手势活跃状态
END: 3, // 3 手势终止
CANCELLED: 4, // 4 手势取消,
}
export const ScrollState = {
scrollStart: 0,
scrollUpdate: 1,
scrollEnd: 2,
}
使用案例
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
const props = defineProps<{
open: boolean
}>()
const _height = workletValue(0)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
applyAnimatedStyle('.collapse-item', () => {
'worklet'
return {
height: _height.value < 0 ? 'auto' : `${_height.value}px`,
}
})
})
watch(() => props.open, () => {
getBoundingClientRect('.collapse-content')
.then(rect => rect.height ?? 0)
.then((height) => {
if (!props.open) {
if (_height.value === -1)
_height.value = height
_height.value = worklet.timing(0, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) })
}
else {
_height.value = worklet.timing(height, { duration: 300, easing: worklet.Easing.inOut(worklet.Easing.ease) }, () => {
'worklet'
if (_height.value === height)
_height.value = -1
})
}
})
})
</script>
<template>
<view class="collapse-item block overflow-hidden">
<view class="collapse-content block">
<slot />
</view>
</view>
</template>
worklet js 互调 inject popup 示例
popup.ts
const [useProvidePopupStore, usePopupStoreInner] = createInjectionState((close: () => void) => {
const _transY = workletValue(1000)
const _scrollTop = workletValue(0)
const _startPan = workletValue(true)
const _popupHeight = workletValue(1000)
return { _transY, _scrollTop, _startPan, _popupHeight, close }
})
export { useProvidePopupStore }
export function usePopupStore() {
const popupStore = usePopupStoreInner()
if (popupStore == null)
throw new Error('Please call `useProvidePopupStore` on the appropriate parent component')
return popupStore
}
export function usePopupWorkletMethods() {
const { _popupHeight, _scrollTop, _startPan, _transY, close } = usePopupStore()
return workletMethods({
openPopup() {
'worklet'
_transY.value = worklet.timing(0, { duration: 200 })
},
closePopup() {
'worklet'
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 })
worklet.runOnJS(close)()
},
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
'worklet'
return _startPan.value
},
shouldScrollViewResponse(pointerEvent: any) {
'worklet'
// transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效
if (_transY.value > 0)
return false
const scrollTop = _scrollTop.value
const { deltaY } = pointerEvent
// deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效
const result = scrollTop <= 0 && deltaY > 0
_startPan.value = result
return !result
},
handlePan(gestureEvent: any) {
'worklet'
if (gestureEvent.state === GestureState.ACTIVE) {
const curPosition = _transY.value
const destination = Math.max(0, curPosition + gestureEvent.deltaY)
if (curPosition === destination)
return
_transY.value = destination
}
if (
gestureEvent.state === GestureState.END
|| gestureEvent.state === GestureState.CANCELLED
) {
if (gestureEvent.velocityY > 500 && _transY.value > 50)
this.closePopup()
else if (_transY.value > _popupHeight.value / 2)
this.closePopup()
else
this.openPopup()
}
},
adjustDecelerationVelocity(velocity: number) {
'worklet'
const scrollTop = _scrollTop.value
return scrollTop <= 0 ? 0 : velocity
},
handleScroll(evt: any) {
'worklet'
_scrollTop.value = evt.detail.scrollTop
},
})
}
popup.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
}
</script>
<script setup lang="ts">
import { useProvidePopupStore } from './popup'
const props = withDefaults(defineProps<{
open: boolean
fullScreen?: boolean
rounded?: boolean
}>(), {
fullScreen: false,
rounded: true,
})
const emit = defineEmits<{
(e: 'update:open', val: boolean): void
(e: 'scrolltolower'): void
(e: 'leave'): void
(e: 'afterLeave'): void
(e: 'enter'): void
(e: 'afterEnter'): void
}>()
function onClose() {
emit('update:open', false)
}
const { _transY, _popupHeight } = useProvidePopupStore(onClose)
const applyAnimatedStyle = useApplyAnimatedStyle()
const { getBoundingClientRect } = useSelectorQuery()
onMounted(() => {
getBoundingClientRect('.popup-container').then((res) => {
_transY.value = _popupHeight.value = res.height ?? 0
})
applyAnimatedStyle('.popup-container', () => {
'worklet'
return {
'transform': `translateY(${_transY.value}px)`,
'box-shadow': `0px 0px ${_transY.value !== _popupHeight.value ? 10 : 0}px 0px rgba(0, 0, 0, 0.2)`,
}
})
applyAnimatedStyle(`.popup-mask`, () => {
'worklet'
return {
opacity: `${1 - _transY.value / _popupHeight.value}`,
display: `${_transY.value !== _popupHeight.value ? 'flex' : 'none'}`,
}
})
})
function onAfterEnter() {
emit('afterEnter')
}
function onAfterLeave() {
emit('afterLeave')
}
watch(() => props.open, () => {
if (props.open) {
emit('enter')
_transY.value = worklet.timing(0, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterEnter)()
})
}
else {
emit('leave')
_transY.value = worklet.timing(_popupHeight.value, { duration: 200 }, () => {
'worklet'
worklet.runOnJS(onAfterLeave)()
})
}
})
</script>
<template>
<root-portal>
<view class="popup-mask absolute left-0 top-0 h-screen w-screen" @tap="onClose" />
<view class="popup-container absolute bottom-0 w-screen overflow-hidden bg-white" :class="[rounded && 'rounded-t-5', fullScreen ? 'h-screen' : 'h-70vh']">
<slot />
</view>
</root-portal>
</template>
<style>
.popup-container {
z-index: 999;
transform: translateY(100%);
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
}
.popup-mask {
z-index: 998;
background-color: rgba(0, 0, 0, 0.5);
display: none;
}
</style>
popup-drag-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
const { handlePan } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler :worklet:ongesture="handlePan.name">
<view :class="mergedClass">
<slot />
</view>
</pan-gesture-handler>
</template>
popup-scroll-view.vue
<script lang="ts">
export default {
options: {
virtualHost: true,
},
behaviors: [virtualHostClassBehavior],
}
</script>
<script setup lang="ts">
import { usePopupWorkletMethods } from './popup'
defineProps<{
type: string
}>()
defineEmits(['scrolltolower'])
const { handlePan, shouldPanResponse, shouldScrollViewResponse, adjustDecelerationVelocity, handleScroll } = usePopupWorkletMethods()
const virtualHostClass = useVirtualHostClass()
const mergedClass = virtualHostClass
</script>
<template>
<pan-gesture-handler
tag="pan"
:worklet:should-response-on-move="shouldPanResponse.name"
:simultaneous-handlers="['scroll']"
:worklet:ongesture="handlePan.name"
>
<vertical-drag-gesture-handler
tag="scroll"
native-view="scroll-view"
:worklet:should-response-on-move="shouldScrollViewResponse.name"
:simultaneous-handlers="['pan']"
>
<scroll-view
:class="mergedClass"
scroll-y
:worklet:adjust-deceleration-velocity="adjustDecelerationVelocity.name"
:worklet:onscrollupdate="handleScroll.name"
type="list"
:show-scrollbar="false"
@scrolltolower="$emit('scrolltolower')"
>
<slot />
</scroll-view>
</vertical-drag-gesture-handler>
</pan-gesture-handler>
</template>
使用方式
<u-popup v-model:open="open">
<u-popup-drag-view v-if="!roomDetail" class="flex-1">
<u-loading />
</u-popup-drag-view>
<u-popup-scroll-view v-else class="flex-1" type="list">
<view class="py-4">
<view
v-for="player in roomDetail?.players"
:key="player.steam"
class="mb-2 flex-row items-center px-4"
@click="onRoomPlayer(player.steam)"
>
<view>
<u-avatar class="h-8 w-8" :src="player.avatar" />
</view>
<view class="ml-2 flex-1 truncate text-sm">
{{ player.name }}
</view>
<view class="ml-2 text-xs text-neutral-500 font-mono">
{{ player.elo }}
</view>
<view class="ml-2" />
</view>
<view class="h-[var(--safe-bottom)]" />
</view>
</u-popup-scroll-view>
</u-popup>
至此,便可以细粒度的调整popup的手势协商
在需要用到scroll-into-view的时候 slot中的元素无法被定位
便可以再次复用usePopupWorkletStore来再次实现定制化的popup-scroll-view
workletMethods的实现中,微信小程序会为具名worklet func 添加 _worklet_factory到包含他的object中 (非具名不会处理) 所以需要全部assgin到mpinstance上
4 个评论
要回复文章请先登录或注册
BoneTM (作者)
app比比
aboutlikefish
BoneTM (作者)