From cf96fa24c8de16122d59c92d6930913e275d7896 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 28 May 2026 19:36:27 +0800 Subject: [PATCH 1/6] =?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/6] =?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/6] =?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(() => { From 9956be69a615f77df94e4c753d2c8752c5481765 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 20 Jul 2026 14:24:25 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20movable-view=20?= =?UTF-8?q?workletChange=20=E9=97=AD=E5=8C=85=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/react/mpx-movable-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7b0f87edad..1461f017d4 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 @@ -755,7 +755,7 @@ const _MovableView = forwardRef, MovableViewP gesturePan.requireExternalGestureToFail(...waitForHandlers) } return gesturePan - }, [disabled, direction, inertia, outOfBounds, gestureSwitch.current]) + }, [disabled, direction, inertia, outOfBounds, gestureSwitch.current, workletChange, handleTriggerWorkletChange]) const animatedStyles = useAnimatedStyle(() => { return { From 3ec5ae4d899b72c4921bac89e660c11b51a0f1a8 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 20 Jul 2026 16:33:16 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20movable-view=20?= =?UTF-8?q?change=20=E4=BA=8B=E4=BB=B6=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-movable-view.tsx | 113 +++++++----------- 1 file changed, 41 insertions(+), 72 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 1461f017d4..967f2302c7 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 @@ -117,7 +117,7 @@ const withWechatDecay = ( currentPosition: number, clampRange: [min: number, max: number], frictionValue = 2, - callback?: () => void + callback?: (finished?: boolean) => void ) => { 'worklet' @@ -147,8 +147,9 @@ const withWechatDecay = ( }, callback) } -type ChangePayload = { x: number; y: number; type?: string } -type ChangeDetail = { x: number; y: number; source: string } +type ChangeSource = '' | 'touch' | 'touch-out-of-bounds' | 'out-of-bounds' | 'friction' +type ChangePayload = { x: number; y: number; source?: ChangeSource } +type ChangeDetail = { x: number; y: number; source: ChangeSource } interface MovableViewProps { children: ReactNode @@ -197,40 +198,22 @@ const styles = StyleSheet.create({ top: 0 } }) -const getChangeSource = ( +const getTouchChangeSource = ( offsetX: number, offsetY: number, xRange: [min: number, max: number], - yRange: [min: number, max: number], - moving: boolean, - inertialMotion: boolean, - currentSource: string -) => { + yRange: [min: number, max: number] +): ChangeSource => { '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 + return hasOverBoundary ? 'touch-out-of-bounds' : 'touch' } const _MovableView = forwardRef, MovableViewProps>((movableViewProps: MovableViewProps, ref): JSX.Element => { const { textProps, innerProps: props = {} } = splitProps(movableViewProps) const movableGestureRef = useRef() const layoutRef = useRef({}) - const bindChangeSource = useRef('') const hasLayoutRef = useRef(false) const propsRef = useRef({}) propsRef.current = (props || {}) as MovableViewProps @@ -295,14 +278,10 @@ const _MovableView = forwardRef, MovableViewP const draggableXRange = useSharedValue<[min: number, max: number]>([0, 0]) const draggableYRange = useSharedValue<[min: number, max: number]>([0, 0]) - const isMoving = useSharedValue(false) - const xInertialMotion = useSharedValue(false) - const yInertialMotion = useSharedValue(false) const isFirstTouch = useSharedValue(true) const touchEvent = useSharedValue('') const initialViewPosition = useSharedValue({ x: x || 0, y: y || 0 }) const lastChangeTime = useSharedValue(0) - const workletChangeSource = useSharedValue('') const MovableAreaLayout = useContext(MovableAreaContext) @@ -329,21 +308,9 @@ const _MovableView = forwardRef, MovableViewP prevSimultaneousHandlersRef.current = originSimultaneousHandlers || [] prevWaitForHandlersRef.current = waitFor || [] - const handleTriggerChange = useCallback(({ x, y, type }: ChangePayload) => { + const handleTriggerChange = useCallback(({ x, y, source = '' }: ChangePayload) => { const { bindchange } = propsRef.current if (!bindchange) return - 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: { @@ -356,21 +323,17 @@ const _MovableView = forwardRef, MovableViewP ) }, []) - const handleTriggerWorkletChange = useCallback(({ x, y, type }: ChangePayload) => { + const handleTriggerWorkletChange = useCallback(({ x, y, source }: ChangePayload) => { 'worklet' - const source = type !== 'setData' - ? getChangeSource( + const changeSource = source === undefined + ? getTouchChangeSource( x, y, draggableXRange.value, - draggableYRange.value, - isMoving.value, - xInertialMotion.value || yInertialMotion.value, - workletChangeSource.value + draggableYRange.value ) - : '' - workletChangeSource.value = source - workletChange && workletChange({ x, y, source }) + : source + workletChange && workletChange({ x, y, source: changeSource }) }, [workletChange]) useEffect(() => { @@ -391,14 +354,14 @@ const _MovableView = forwardRef, MovableViewP handleTriggerWorkletChange({ x: newX, y: newY, - type: 'setData' + source: '' }) } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: newX, y: newY, - type: 'setData' + source: '' }) } } @@ -558,12 +521,18 @@ const _MovableView = forwardRef, MovableViewP const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef) // 节流版本的change事件触发 - const handleTriggerChangeThrottled = useCallback(({ x, y, type }: { x: number; y: number; type?: string }) => { + const handleTriggerChangeThrottled = useCallback(({ x, y }: { x: number; y: number }) => { 'worklet' const now = Date.now() if (now - lastChangeTime.value >= changeThrottleTime) { lastChangeTime.value = now - runOnJS(runOnJSCallback)('handleTriggerChange', { x, y, type }) + const source = getTouchChangeSource( + x, + y, + draggableXRange.value, + draggableYRange.value + ) + runOnJS(runOnJSCallback)('handleTriggerChange', { x, y, source }) } }, [changeThrottleTime]) @@ -586,7 +555,6 @@ const _MovableView = forwardRef, MovableViewP .onTouchesDown((e: GestureTouchEvent) => { 'worklet' const changedTouches = e.changedTouches[0] || { x: 0, y: 0 } - isMoving.value = false startPosition.value = { x: changedTouches.x, y: changedTouches.y @@ -605,7 +573,6 @@ const _MovableView = forwardRef, MovableViewP .onTouchesMove((e: GestureTouchEvent) => { 'worklet' const changedTouches = e.changedTouches[0] || { x: 0, y: 0 } - isMoving.value = true if (isFirstTouch.value) { touchEvent.value = Math.abs(changedTouches.x - startPosition.value.x) > Math.abs(changedTouches.y - startPosition.value.y) ? 'htouchmove' : 'vtouchmove' isFirstTouch.value = false @@ -649,14 +616,12 @@ const _MovableView = forwardRef, MovableViewP .onTouchesUp((e: GestureTouchEvent) => { 'worklet' isFirstTouch.value = true - isMoving.value = false if (bindtouchend || catchtouchend) { runOnJS(runOnJSCallback)('triggerEndOnJS', { e }) } }) .onEnd((e: GestureStateChangeEvent) => { 'worklet' - isMoving.value = false if (disabled) return // 处理没有惯性且超出边界的回弹 if (!inertia && outOfBounds) { @@ -675,61 +640,65 @@ const _MovableView = forwardRef, MovableViewP if (workletChange) { handleTriggerWorkletChange({ x, - y + y, + source: 'out-of-bounds' }) } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x, - y + y, + source: 'out-of-bounds' }) } } } else if (inertia) { // 惯性处理 - 使用微信小程序friction算法 if (direction === 'horizontal' || direction === 'all') { - xInertialMotion.value = true offsetX.value = withWechatDecay( e.velocityX / 10, offsetX.value, draggableXRange.value, friction, - () => { - xInertialMotion.value = false + (finished) => { + if (!finished) return if (workletChange) { handleTriggerWorkletChange({ x: offsetX.value, - y: offsetY.value + y: offsetY.value, + source: 'friction' }) } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: offsetX.value, - y: offsetY.value + y: offsetY.value, + source: 'friction' }) } } ) } if (direction === 'vertical' || direction === 'all') { - yInertialMotion.value = true offsetY.value = withWechatDecay( e.velocityY / 10, offsetY.value, draggableYRange.value, friction, - () => { - yInertialMotion.value = false + (finished) => { + if (!finished) return if (workletChange) { handleTriggerWorkletChange({ x: offsetX.value, - y: offsetY.value + y: offsetY.value, + source: 'friction' }) } if (bindchange) { runOnJS(runOnJSCallback)('handleTriggerChange', { x: offsetX.value, - y: offsetY.value + y: offsetY.value, + source: 'friction' }) } } From fb38cd1326d86fe47f9a5e8fcd2df0cf4b94859a Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 27 Jul 2026 12:09:03 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20movable-view=20?= =?UTF-8?q?change=20=E4=BA=8B=E4=BB=B6=E8=A7=A6=E5=8F=91=E6=97=B6=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/react/mpx-movable-view.tsx | 204 +++++++++--------- 1 file changed, 96 insertions(+), 108 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 967f2302c7..96705dc1bf 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 @@ -31,7 +31,8 @@ import Animated, { runOnJS, runOnUI, withTiming, - Easing + Easing, + useAnimatedReaction } from 'react-native-reanimated' import { collectDataset, noop } from '@mpxjs/utils' @@ -61,7 +62,7 @@ const applyBoundaryDecline = ( const withWechatSpring = ( toValue: number, dampingParam = 20, - callback?: () => void + callback?: (finished?: boolean) => void ) => { 'worklet' @@ -250,6 +251,7 @@ const _MovableView = forwardRef, MovableViewP bindchange, workletChange } = props + const hasBindChange = !!bindchange const { hasSelfPercent, @@ -282,6 +284,8 @@ const _MovableView = forwardRef, MovableViewP const touchEvent = useSharedValue('') const initialViewPosition = useSharedValue({ x: x || 0, y: y || 0 }) const lastChangeTime = useSharedValue(0) + const lastChangePosition = useSharedValue({ x, y }) + const changeSource = useSharedValue('') const MovableAreaLayout = useContext(MovableAreaContext) @@ -323,47 +327,83 @@ const _MovableView = forwardRef, MovableViewP ) }, []) - const handleTriggerWorkletChange = useCallback(({ x, y, source }: ChangePayload) => { + const bindChangeCallbackRef = useRef({ handleTriggerChange }) + const runOnJSBindChangeCallback = useRunOnJSCallback(bindChangeCallbackRef) + + const triggerBindChange = useCallback(({ x, y }: { x: number; y: number }, bypassThrottle = false) => { 'worklet' - const changeSource = source === undefined + if (x === lastChangePosition.value.x && y === lastChangePosition.value.y) return + const now = Date.now() + if (!bypassThrottle && now - lastChangeTime.value < changeThrottleTime) return + lastChangeTime.value = now + lastChangePosition.value = { x, y } + const source = changeSource.value === 'touch' ? getTouchChangeSource( x, y, draggableXRange.value, draggableYRange.value ) - : source - workletChange && workletChange({ x, y, source: changeSource }) - }, [workletChange]) + : changeSource.value + runOnJS(runOnJSBindChangeCallback)('handleTriggerChange', { x, y, source }) + }, [changeThrottleTime]) + + const flushBindChange = useCallback((finished = true) => { + 'worklet' + if (!finished || !hasBindChange) return + triggerBindChange({ + x: offsetX.value, + y: offsetY.value + }, true) + }, [hasBindChange, triggerBindChange]) + + useAnimatedReaction( + () => ({ + x: offsetX.value, + y: offsetY.value + }), + (current, previous) => { + if ( + !previous || + (current.x === previous.x && current.y === previous.y) + ) return + if (workletChange) { + const source = changeSource.value === 'touch' + ? getTouchChangeSource( + current.x, + current.y, + draggableXRange.value, + draggableYRange.value + ) + : changeSource.value + workletChange({ + x: current.x, + y: current.y, + source + }) + } + if (hasBindChange) { + triggerBindChange(current) + } + } + ) useEffect(() => { runOnUI(() => { if (offsetX.value !== x || offsetY.value !== y) { const { x: newX, y: newY } = checkBoundaryPosition({ positionX: Number(x), positionY: Number(y) }) + changeSource.value = '' if (direction === 'horizontal' || direction === 'all') { offsetX.value = animation - ? withWechatSpring(newX, damping) + ? withWechatSpring(newX, damping, flushBindChange) : newX } if (direction === 'vertical' || direction === 'all') { offsetY.value = animation - ? withWechatSpring(newY, damping) + ? withWechatSpring(newY, damping, flushBindChange) : newY } - if (workletChange) { - handleTriggerWorkletChange({ - x: newX, - y: newY, - source: '' - }) - } - if (bindchange) { - runOnJS(runOnJSCallback)('handleTriggerChange', { - x: newX, - y: newY, - source: '' - }) - } + if (!animation) flushBindChange() } })() }, [x, y]) @@ -429,11 +469,15 @@ const _MovableView = forwardRef, MovableViewP const positionX = offsetX.value const positionY = offsetY.value const { x: newX, y: newY } = checkBoundaryPosition({ positionX, positionY }) - if (positionX !== newX) { - offsetX.value = newX - } - if (positionY !== newY) { - offsetY.value = newY + if (positionX !== newX || positionY !== newY) { + changeSource.value = 'out-of-bounds' + if (positionX !== newX) { + offsetX.value = newX + } + if (positionY !== newY) { + offsetY.value = newY + } + flushBindChange() } })() } @@ -513,29 +557,12 @@ const _MovableView = forwardRef, MovableViewP } const runOnJSCallbackRef = useRef({ - handleTriggerChange, triggerStartOnJS, triggerMoveOnJS, triggerEndOnJS }) const runOnJSCallback = useRunOnJSCallback(runOnJSCallbackRef) - // 节流版本的change事件触发 - const handleTriggerChangeThrottled = useCallback(({ x, y }: { x: number; y: number }) => { - 'worklet' - const now = Date.now() - if (now - lastChangeTime.value >= changeThrottleTime) { - lastChangeTime.value = now - const source = getTouchChangeSource( - x, - y, - draggableXRange.value, - draggableYRange.value - ) - runOnJS(runOnJSCallback)('handleTriggerChange', { x, y, source }) - } - }, [changeThrottleTime]) - const gesture = useMemo(() => { const handleTriggerMove = (e: GestureTouchEvent) => { 'worklet' @@ -582,6 +609,7 @@ const _MovableView = forwardRef, MovableViewP .onUpdate((e: GestureStateChangeEvent) => { 'worklet' if (disabled) return + changeSource.value = 'touch' if (direction === 'horizontal' || direction === 'all') { const newX = initialViewPosition.value.x + e.translationX if (!outOfBounds) { @@ -600,18 +628,6 @@ const _MovableView = forwardRef, MovableViewP offsetY.value = applyBoundaryDecline(newY, draggableYRange.value) } } - if (workletChange) { - handleTriggerWorkletChange({ - x: offsetX.value, - y: offsetY.value - }) - } - if (bindchange) { - handleTriggerChangeThrottled({ - x: offsetX.value, - y: offsetY.value - }) - } }) .onTouchesUp((e: GestureTouchEvent) => { 'worklet' @@ -627,56 +643,31 @@ const _MovableView = forwardRef, MovableViewP if (!inertia && outOfBounds) { const { x, y } = checkBoundaryPosition({ positionX: offsetX.value, positionY: offsetY.value }) if (x !== offsetX.value || y !== offsetY.value) { + changeSource.value = 'out-of-bounds' if (x !== offsetX.value) { offsetX.value = animation - ? withWechatSpring(x, damping) + ? withWechatSpring(x, damping, flushBindChange) : x } if (y !== offsetY.value) { offsetY.value = animation - ? withWechatSpring(y, damping) + ? withWechatSpring(y, damping, flushBindChange) : y } - if (workletChange) { - handleTriggerWorkletChange({ - x, - y, - source: 'out-of-bounds' - }) - } - if (bindchange) { - runOnJS(runOnJSCallback)('handleTriggerChange', { - x, - y, - source: 'out-of-bounds' - }) - } + if (!animation) flushBindChange() + } else { + flushBindChange() } } else if (inertia) { // 惯性处理 - 使用微信小程序friction算法 + changeSource.value = 'friction' if (direction === 'horizontal' || direction === 'all') { offsetX.value = withWechatDecay( e.velocityX / 10, offsetX.value, draggableXRange.value, friction, - (finished) => { - if (!finished) return - if (workletChange) { - handleTriggerWorkletChange({ - x: offsetX.value, - y: offsetY.value, - source: 'friction' - }) - } - if (bindchange) { - runOnJS(runOnJSCallback)('handleTriggerChange', { - x: offsetX.value, - y: offsetY.value, - source: 'friction' - }) - } - } + flushBindChange ) } if (direction === 'vertical' || direction === 'all') { @@ -685,25 +676,11 @@ const _MovableView = forwardRef, MovableViewP offsetY.value, draggableYRange.value, friction, - (finished) => { - if (!finished) return - if (workletChange) { - handleTriggerWorkletChange({ - x: offsetX.value, - y: offsetY.value, - source: 'friction' - }) - } - if (bindchange) { - runOnJS(runOnJSCallback)('handleTriggerChange', { - x: offsetX.value, - y: offsetY.value, - source: 'friction' - }) - } - } + flushBindChange ) } + } else { + flushBindChange() } }) .withRef(movableGestureRef) @@ -724,7 +701,18 @@ const _MovableView = forwardRef, MovableViewP gesturePan.requireExternalGestureToFail(...waitForHandlers) } return gesturePan - }, [disabled, direction, inertia, outOfBounds, gestureSwitch.current, workletChange, handleTriggerWorkletChange]) + }, [ + disabled, + direction, + inertia, + outOfBounds, + animation, + damping, + friction, + disableEventPassthrough, + flushBindChange, + gestureSwitch.current + ]) const animatedStyles = useAnimatedStyle(() => { return {