diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 252e442e1e..19603fcaa3 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -753,6 +753,7 @@ movable-view 的可移动区域。 | animation | boolean | `true` | 是否使用动画 | | damping | number | `20` | 阻尼系数,用于控制 x 或 y 改变时的动画和过界回弹的动画,值越大移动越快 | | friction | number | `2` | 摩擦系数,用于控制惯性滑动的动画,值越大摩擦力越大,滑动越快停止 | +| workletChange | function | | RN 环境特有属性,拖动位置变化时在 UI 线程立即触发,回调参数为 `{x, y, source}`,不受 `changeThrottleTime` 影响 | | simultaneous-handlers | array\ | `[]` | RN 环境特有属性,主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件 | | wait-for | array\ | `[]` | RN 环境特有属性,主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件 | | disable-event-passthrough | boolean | `false` | RN 环境特有属性,有时候我们希望 movable-view 在水平方向滑动,并且竖直方向的手势也希望被 movable-view 组件消费掉,不被其他组件响应,可以将这个属性设置为 true) | @@ -769,6 +770,7 @@ movable-view 的可移动区域。 - simultaneous-handlers 为 RN 环境特有属性,具体含义可参考[react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture) - wait-for 为 RN 环境特有属性,具体含义可参考[react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail) +- workletChange 为 RN 环境特有属性,回调会在 UI 线程执行,函数体需包含 `'worklet'` 指令,且只接收 `{x, y, source}` 参数,不是完整 Mpx 事件对象 - RN 环境 movable 相关组件暂不支持缩放能力 ### image diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index bfe0fb3a56..4125772f3e 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -210,6 +210,7 @@ movable-view的可移动区域。 | animation | boolean | `true` | 是否使用动画 | | damping | number | `20` | 阻尼系数,用于控制x或y改变时的动画和过界回弹的动画,值越大移动越快 | | friction | number | `2` | 摩擦系数,用于控制惯性滑动的动画,值越大摩擦力越大,滑动越快停止 | +| workletChange | function | | RN 环境特有属性,拖动位置变化时在 UI 线程立即触发,回调参数为 `{x, y, source}`,不受 `changeThrottleTime` 影响 | | simultaneous-handlers | array\ | `[]` | RN 环境特有属性,主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件 | | wait-for | array\ | `[]` | RN 环境特有属性,主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件 | | disable-event-passthrough | boolean | `false` | RN 环境特有属性,有时候我们希望movable-view 在水平方向滑动,并且竖直方向的手势也希望被 movable-view 组件消费掉,不被其他组件响应,可以将这个属性设置为true) | @@ -227,6 +228,7 @@ movable-view的可移动区域。 > > - simultaneous-handlers 为 RN 环境特有属性,具体含义可参考[react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture) > - wait-for 为 RN 环境特有属性,具体含义可参考[react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail) +> - workletChange 为 RN 环境特有属性,回调会在 UI 线程执行,函数体需包含 `'worklet'` 指令,且只接收 `{x, y, source}` 参数,不是完整 Mpx 事件对象 > - RN 环境 movable 相关组件暂不支持缩放能力 ### image diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx index d27254ed12..489e1c5788 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-movable-view.tsx @@ -13,6 +13,7 @@ * ✘ scale-value * ✔ animation * ✔ bindchange + * ✔ workletChange * ✘ bindscale * ✔ htouchmove * ✔ vtouchmove @@ -146,6 +147,9 @@ const withWechatDecay = ( }, callback) } +type ChangePayload = { x: number; y: number; type?: string } +type ChangeDetail = { x: number; y: number; source: string } + interface MovableViewProps { children: ReactNode style?: Record @@ -159,6 +163,7 @@ interface MovableViewProps { id?: string changeThrottleTime?:number bindchange?: (event: unknown) => void + workletChange?: (detail: ChangeDetail) => void bindtouchstart?: (event: GestureTouchEvent) => void catchtouchstart?: (event: GestureTouchEvent) => void bindtouchmove?: (event: GestureTouchEvent) => void @@ -193,12 +198,40 @@ const styles = StyleSheet.create({ top: 0 } }) +const getChangeSource = ( + offsetX: number, + offsetY: number, + xRange: [min: number, max: number], + yRange: [min: number, max: number], + moving: boolean, + inertialMotion: boolean, + currentSource: string +) => { + 'worklet' + const hasOverBoundary = offsetX < xRange[0] || offsetX > xRange[1] || + offsetY < yRange[0] || offsetY > yRange[1] + let source = currentSource + if (hasOverBoundary) { + if (moving) { + source = 'touch-out-of-bounds' + } else { + source = 'out-of-bounds' + } + } else { + if (moving) { + source = 'touch' + } else if (inertialMotion && (currentSource === 'touch' || currentSource === 'friction')) { + source = 'friction' + } + } + return source +} const _MovableView = forwardRef, MovableViewProps>((movableViewProps: MovableViewProps, ref): JSX.Element => { const { textProps, innerProps: props = {} } = splitProps(movableViewProps) const movableGestureRef = useRef() const layoutRef = useRef({}) - const changeSource = useRef('') + const bindChangeSource = useRef('') const hasLayoutRef = useRef(false) const propsRef = useRef({}) propsRef.current = (props || {}) as MovableViewProps @@ -233,7 +266,8 @@ const _MovableView = forwardRef, MovableViewP catchtouchmove, bindtouchend, catchtouchend, - bindchange + bindchange, + workletChange } = props const { @@ -270,6 +304,7 @@ const _MovableView = forwardRef, MovableViewP const touchEvent = useSharedValue('') const initialViewPosition = useSharedValue({ x: x || 0, y: y || 0 }) const lastChangeTime = useSharedValue(0) + const workletChangeSource = useSharedValue('') const MovableAreaLayout = useContext(MovableAreaContext) @@ -296,15 +331,21 @@ const _MovableView = forwardRef, MovableViewP prevSimultaneousHandlersRef.current = originSimultaneousHandlers || [] prevWaitForHandlersRef.current = waitFor || [] - const handleTriggerChange = useCallback(({ x, y, type }: { x: number; y: number; type?: string }) => { + const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { const { bindchange } = propsRef.current if (!bindchange) return - let source = '' - if (type !== 'setData') { - source = getTouchSource(x, y) - } else { - changeSource.current = '' - } + const source = type !== 'setData' + ? getChangeSource( + x, + y, + draggableXRange.value, + draggableYRange.value, + isMoving.value, + xInertialMotion.value || yInertialMotion.value, + bindChangeSource.current + ) + : '' + bindChangeSource.current = source bindchange( getCustomEvent('change', {}, { detail: { @@ -317,6 +358,23 @@ const _MovableView = forwardRef, MovableViewP ) }, []) + const handleTriggerWorkletChange = useCallback(({ x, y, type }: ChangePayload) => { + 'worklet' + const source = type !== 'setData' + ? getChangeSource( + x, + y, + draggableXRange.value, + draggableYRange.value, + isMoving.value, + xInertialMotion.value || yInertialMotion.value, + workletChangeSource.value + ) + : '' + workletChangeSource.value = source + workletChange && workletChange({ x, y, source }) + }, [workletChange]) + useEffect(() => { runOnUI(() => { if (offsetX.value !== x || offsetY.value !== y) { @@ -331,6 +389,13 @@ const _MovableView = forwardRef, MovableViewP ? withWechatSpring(newY, damping) : newY } + if (workletChange) { + handleTriggerWorkletChange({ + x: newX, + y: newY, + type: 'setData' + }) + } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: newX, @@ -349,27 +414,6 @@ const _MovableView = forwardRef, MovableViewP } }, [MovableAreaLayout.height, MovableAreaLayout.width]) - const getTouchSource = useCallback((offsetX: number, offsetY: number) => { - const hasOverBoundary = offsetX < draggableXRange.value[0] || offsetX > draggableXRange.value[1] || - offsetY < draggableYRange.value[0] || offsetY > draggableYRange.value[1] - let source = changeSource.current - if (hasOverBoundary) { - if (isMoving.value) { - source = 'touch-out-of-bounds' - } else { - source = 'out-of-bounds' - } - } else { - if (isMoving.value) { - source = 'touch' - } else if ((xInertialMotion.value || yInertialMotion.value) && (changeSource.current === 'touch' || changeSource.current === 'friction')) { - source = 'friction' - } - } - changeSource.current = source - return source - }, []) - const setBoundary = useCallback(({ width, height }: { width: number; height: number }) => { const top = (style.position === 'absolute' && style.top) || 0 const left = (style.position === 'absolute' && style.left) || 0 @@ -591,8 +635,13 @@ const _MovableView = forwardRef, MovableViewP offsetY.value = applyBoundaryDecline(newY, draggableYRange.value) } } + if (workletChange) { + handleTriggerWorkletChange({ + x: offsetX.value, + y: offsetY.value + }) + } if (bindchange) { - // 使用节流版本减少 runOnJS 调用 handleTriggerChangeThrottled({ x: offsetX.value, y: offsetY.value @@ -625,6 +674,12 @@ const _MovableView = forwardRef, MovableViewP ? withWechatSpring(y, damping) : y } + if (workletChange) { + handleTriggerWorkletChange({ + x, + y + }) + } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x, @@ -643,6 +698,12 @@ const _MovableView = forwardRef, MovableViewP friction, () => { xInertialMotion.value = false + if (workletChange) { + handleTriggerWorkletChange({ + x: offsetX.value, + y: offsetY.value + }) + } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: offsetX.value, @@ -661,6 +722,12 @@ const _MovableView = forwardRef, MovableViewP friction, () => { yInertialMotion.value = false + if (workletChange) { + handleTriggerWorkletChange({ + x: offsetX.value, + y: offsetY.value + }) + } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: offsetX.value, @@ -733,7 +800,8 @@ const _MovableView = forwardRef, MovableViewP 'catchtouchmove', 'catchvtouchmove', 'catchhtouchmove', - 'catchtouchend' + 'catchtouchend', + 'workletChange' ]) const innerProps = useInnerProps(