From cf96fa24c8de16122d59c92d6930913e275d7896 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 28 May 2026 19:36:27 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20movable-view=20change=20=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E6=94=AF=E6=8C=81UI=E7=BA=BF=E7=A8=8B=E8=B0=83?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../references/rn-template-reference.md | 2 + docs-vitepress/guide/rn/component.md | 2 + .../components/react/mpx-movable-view.tsx | 138 ++++++++++++++---- 3 files changed, 114 insertions(+), 28 deletions(-) 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..1a4dc17997 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,38 @@ const withWechatDecay = ( }, callback) } +type ChangePayload = { x: number; y: number; type?: string } +type ChangeDetail = { x: number; y: number; source: string } + +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 +} + interface MovableViewProps { children: ReactNode style?: Record @@ -159,6 +192,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 @@ -198,7 +232,7 @@ const _MovableView = forwardRef, MovableViewP 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 +267,8 @@ const _MovableView = forwardRef, MovableViewP catchtouchmove, bindtouchend, catchtouchend, - bindchange + bindchange, + workletChange } = props const { @@ -270,6 +305,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,14 +332,45 @@ const _MovableView = forwardRef, MovableViewP prevSimultaneousHandlersRef.current = originSimultaneousHandlers || [] prevWaitForHandlersRef.current = waitFor || [] - const handleTriggerChange = useCallback(({ x, y, type }: { x: number; y: number; type?: string }) => { + const getWorkletChangeDetail = 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 + return { x, y, source } + }, []) + + const getBindTouchSource = useCallback((offsetX: number, offsetY: number) => { + const source = getChangeSource( + offsetX, + offsetY, + draggableXRange.value, + draggableYRange.value, + isMoving.value, + xInertialMotion.value || yInertialMotion.value, + bindChangeSource.current + ) + bindChangeSource.current = source + return source + }, []) + + const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { const { bindchange } = propsRef.current if (!bindchange) return let source = '' if (type !== 'setData') { - source = getTouchSource(x, y) + source = getBindTouchSource(x, y) } else { - changeSource.current = '' + bindChangeSource.current = '' } bindchange( getCustomEvent('change', {}, { @@ -317,6 +384,11 @@ const _MovableView = forwardRef, MovableViewP ) }, []) + const handleTriggerWorkletChange = useCallback(({ x, y, type }: ChangePayload) => { + 'worklet' + workletChange && workletChange(getWorkletChangeDetail({ x, y, type })) + }, [workletChange]) + useEffect(() => { runOnUI(() => { if (offsetX.value !== x || offsetY.value !== y) { @@ -331,6 +403,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 +428,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 +649,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 +688,12 @@ const _MovableView = forwardRef, MovableViewP ? withWechatSpring(y, damping) : y } + if (workletChange) { + handleTriggerWorkletChange({ + x, + y + }) + } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x, @@ -643,6 +712,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 +736,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 +814,8 @@ const _MovableView = forwardRef, MovableViewP 'catchtouchmove', 'catchvtouchmove', 'catchhtouchmove', - 'catchtouchend' + 'catchtouchend', + 'workletChange' ]) const innerProps = useInnerProps( From 15d34d184b9d10656c465b9ac32676a0cf311705 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Fri, 29 May 2026 16:07:21 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=20movable-vi?= =?UTF-8?q?ew=20change=20source=20=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-movable-view.tsx | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) 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 1a4dc17997..5cd856e38a 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 @@ -150,35 +150,6 @@ const withWechatDecay = ( type ChangePayload = { x: number; y: number; type?: string } type ChangeDetail = { x: number; y: number; source: string } -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 -} - interface MovableViewProps { children: ReactNode style?: Record @@ -227,6 +198,34 @@ 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) @@ -350,7 +349,7 @@ const _MovableView = forwardRef, MovableViewP }, []) const getBindTouchSource = useCallback((offsetX: number, offsetY: number) => { - const source = getChangeSource( + return getChangeSource( offsetX, offsetY, draggableXRange.value, @@ -359,19 +358,13 @@ const _MovableView = forwardRef, MovableViewP xInertialMotion.value || yInertialMotion.value, bindChangeSource.current ) - bindChangeSource.current = source - return source }, []) const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { const { bindchange } = propsRef.current if (!bindchange) return - let source = '' - if (type !== 'setData') { - source = getBindTouchSource(x, y) - } else { - bindChangeSource.current = '' - } + const source = type !== 'setData' ? getBindTouchSource(x, y) : '' + bindChangeSource.current = source bindchange( getCustomEvent('change', {}, { detail: { From bd3bfba4eda6b7f96d21f24f362f0d6b07fec3fb Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Fri, 29 May 2026 16:27:56 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=E7=AE=80=E5=8C=96=20movable-vi?= =?UTF-8?q?ew=20change=20source=20=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-movable-view.tsx | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) 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 5cd856e38a..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 @@ -331,8 +331,9 @@ const _MovableView = forwardRef, MovableViewP prevSimultaneousHandlersRef.current = originSimultaneousHandlers || [] prevWaitForHandlersRef.current = waitFor || [] - const getWorkletChangeDetail = useCallback(({ x, y, type }: ChangePayload) => { - 'worklet' + const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { + const { bindchange } = propsRef.current + if (!bindchange) return const source = type !== 'setData' ? getChangeSource( x, @@ -341,29 +342,9 @@ const _MovableView = forwardRef, MovableViewP draggableYRange.value, isMoving.value, xInertialMotion.value || yInertialMotion.value, - workletChangeSource.value + bindChangeSource.current ) : '' - workletChangeSource.value = source - return { x, y, source } - }, []) - - const getBindTouchSource = useCallback((offsetX: number, offsetY: number) => { - return getChangeSource( - offsetX, - offsetY, - draggableXRange.value, - draggableYRange.value, - isMoving.value, - xInertialMotion.value || yInertialMotion.value, - bindChangeSource.current - ) - }, []) - - const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { - const { bindchange } = propsRef.current - if (!bindchange) return - const source = type !== 'setData' ? getBindTouchSource(x, y) : '' bindChangeSource.current = source bindchange( getCustomEvent('change', {}, { @@ -379,7 +360,19 @@ const _MovableView = forwardRef, MovableViewP const handleTriggerWorkletChange = useCallback(({ x, y, type }: ChangePayload) => { 'worklet' - workletChange && workletChange(getWorkletChangeDetail({ x, y, type })) + 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(() => {