BoneTM
BoneTM
  • 发布:2024-04-28 12:56
  • 更新:2024-05-04 11:42
  • 阅读:96

skyline worklet 纯 setup 写法分享 ts支持 全场景支持

分类:uni-app

仅在微信小程序测试,多端请自行添加条件编译

首先是一个 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上

0 关注 分享

要回复文章请先登录注册

aboutlikefish

aboutlikefish

费老大劲,但是表示支持
2024-05-04 11:42
BoneTM

BoneTM (作者)

:worklet:ongesture="handlePan.name"
的写法可以换成
worklet:ongesture="handlePan"
因为func名其实是保留的
2024-04-28 13:02