@mpxjs/webpack-plugin 2.10.7-beta.8 → 2.10.7-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -285,6 +285,13 @@ module.exports = function getSpec ({ warn, error }) {
285
285
 
286
286
  // line-height
287
287
  const formatLineHeight = ({ prop, value, selector }) => {
288
+ // line-height 0 直接返回
289
+ if (+value === 0) {
290
+ return {
291
+ prop,
292
+ value
293
+ }
294
+ }
288
295
  return verifyValues({ prop, value, selector }) && ({
289
296
  prop,
290
297
  value: /^\s*(-?(\d+(\.\d+)?|\.\d+))\s*$/.test(value) ? `${Math.round(value * 100)}%` : value
@@ -2,9 +2,6 @@ const TAG_NAME = 'movable-view'
2
2
 
3
3
  module.exports = function ({ print }) {
4
4
  const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' })
5
- const androidEventLog = print({ platform: 'android', tag: TAG_NAME, isError: false, type: 'event' })
6
- const harmonyEventLog = print({ platform: 'harmony', tag: TAG_NAME, isError: false, type: 'event' })
7
- const iosEventLog = print({ platform: 'ios', tag: TAG_NAME, isError: false, type: 'event' })
8
5
  const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false })
9
6
  const androidPropLog = print({ platform: 'android', tag: TAG_NAME, isError: false })
10
7
  const harmonyPropLog = print({ platform: 'harmony', tag: TAG_NAME, isError: false })
@@ -36,7 +33,7 @@ module.exports = function ({ print }) {
36
33
  harmony: harmonyPropLog
37
34
  },
38
35
  {
39
- test: /^(damping|friction|scale|scale-min|scale-max|scale-value)$/,
36
+ test: /^(damping|friction)$/,
40
37
  ios: iosPropLog,
41
38
  android: androidPropLog,
42
39
  harmony: harmonyPropLog
@@ -46,12 +43,6 @@ module.exports = function ({ print }) {
46
43
  {
47
44
  test: /^(htouchmove|vtouchmove)$/,
48
45
  ali: aliEventLog
49
- },
50
- {
51
- test: /^(bindscale)$/,
52
- ios: iosEventLog,
53
- android: androidEventLog,
54
- harmony: harmonyEventLog
55
46
  }
56
47
  ]
57
48
  }
@@ -51,14 +51,24 @@ export interface RouteContextValue {
51
51
  pageId: number
52
52
  navigation: Record<string, any>
53
53
  }
54
+ export interface MovableAreaContextValue {
55
+ width: number
56
+ height: number
57
+ scaleArea: boolean
58
+ onAreaScale?: (scaleInfo: { scale: number }) => void
59
+ registerMovableView?: (id: string, callbacks: {
60
+ onScale: (scaleInfo: { scale: number }) => void
61
+ onScaleEnd?: () => void
62
+ }) => void
63
+ unregisterMovableView?: (id: string) => void
64
+ }
54
65
 
66
+ export const MovableAreaContext = createContext<MovableAreaContextValue>({ width: 0, height: 0, scaleArea: false })
55
67
  export interface StickyContextValue {
56
68
  registerStickyHeader: Function,
57
69
  unregisterStickyHeader: Function
58
70
  }
59
71
 
60
- export const MovableAreaContext = createContext({ width: 0, height: 0 })
61
-
62
72
  export const FormContext = createContext<FormContextValue | null>(null)
63
73
 
64
74
  export const CheckboxGroupContext = createContext<GroupContextValue | null>(null)
@@ -1,7 +1,7 @@
1
1
  import { createContext } from 'react';
2
2
  import { Animated } from 'react-native';
3
3
  import { noop } from '@mpxjs/utils';
4
- export const MovableAreaContext = createContext({ width: 0, height: 0 });
4
+ export const MovableAreaContext = createContext({ width: 0, height: 0, scaleArea: false });
5
5
  export const FormContext = createContext(null);
6
6
  export const CheckboxGroupContext = createContext(null);
7
7
  export const RadioGroupContext = createContext(null);
@@ -1,33 +1,87 @@
1
1
  /**
2
- * scale-area
2
+ * scale-area
3
3
  */
4
4
  import { View } from 'react-native';
5
- import { forwardRef, useRef, useMemo, createElement } from 'react';
5
+ import { forwardRef, useRef, useMemo, useCallback, createElement } from 'react';
6
+ import { GestureDetector, Gesture } from 'react-native-gesture-handler';
7
+ import { useSharedValue } from 'react-native-reanimated';
6
8
  import useNodesRef from './useNodesRef';
7
9
  import useInnerProps from './getInnerListeners';
8
10
  import { MovableAreaContext } from './context';
9
11
  import { useTransformStyle, wrapChildren, useLayout, extendObject } from './utils';
10
12
  import Portal from './mpx-portal';
11
13
  const _MovableArea = forwardRef((props, ref) => {
12
- const { style = {}, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
14
+ const { style = {}, 'scale-area': scaleArea = false, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
13
15
  const { hasSelfPercent, normalStyle, hasVarDec, varContextRef, hasPositionFixed, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
14
- const movableViewRef = useRef(null);
15
- useNodesRef(props, ref, movableViewRef, {
16
+ const movableAreaRef = useRef(null);
17
+ const movableViewsValue = useSharedValue({});
18
+ useNodesRef(props, ref, movableAreaRef, {
16
19
  style: normalStyle
17
20
  });
21
+ // 注册/注销 MovableView 的回调
22
+ const registerMovableView = useCallback((id, callbacks) => {
23
+ movableViewsValue.value = extendObject(movableViewsValue.value, { [id]: callbacks });
24
+ }, []);
25
+ const unregisterMovableView = useCallback((id) => {
26
+ delete movableViewsValue.value[id];
27
+ }, []);
28
+ // 处理区域缩放手势
29
+ const handleAreaScale = useCallback((scaleInfo) => {
30
+ 'worklet';
31
+ if (scaleArea) {
32
+ // 将缩放信息广播给所有注册的 MovableView
33
+ Object.values(movableViewsValue.value).forEach((callbacks) => {
34
+ callbacks.onScale && callbacks.onScale(scaleInfo);
35
+ });
36
+ }
37
+ }, [scaleArea]);
38
+ // 处理区域缩放结束
39
+ const handleAreaScaleEnd = useCallback(() => {
40
+ 'worklet';
41
+ if (scaleArea) {
42
+ // 通知所有注册的 MovableView 缩放结束
43
+ Object.values(movableViewsValue.value).forEach((callbacks) => {
44
+ callbacks.onScaleEnd && callbacks.onScaleEnd();
45
+ });
46
+ }
47
+ }, [scaleArea]);
18
48
  const contextValue = useMemo(() => ({
19
49
  height: normalStyle.height || 10,
20
- width: normalStyle.width || 10
21
- }), [normalStyle.width, normalStyle.height]);
22
- const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: movableViewRef });
50
+ width: normalStyle.width || 10,
51
+ scaleArea,
52
+ registerMovableView,
53
+ unregisterMovableView
54
+ }), [normalStyle.width, normalStyle.height, scaleArea]);
55
+ const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: movableAreaRef });
56
+ // 创建缩放手势
57
+ const scaleGesture = useMemo(() => {
58
+ if (!scaleArea)
59
+ return null;
60
+ return Gesture.Pinch()
61
+ .onUpdate((e) => {
62
+ 'worklet';
63
+ handleAreaScale(e);
64
+ })
65
+ .onEnd(() => {
66
+ 'worklet';
67
+ handleAreaScaleEnd();
68
+ });
69
+ }, [scaleArea]);
23
70
  const innerProps = useInnerProps(extendObject({}, props, layoutProps, {
24
71
  style: extendObject({ height: contextValue.height, width: contextValue.width }, normalStyle, layoutStyle),
25
- ref: movableViewRef
72
+ ref: movableAreaRef
26
73
  }), [], { layoutRef });
27
74
  let movableComponent = createElement(MovableAreaContext.Provider, { value: contextValue }, createElement(View, innerProps, wrapChildren(props, {
28
75
  hasVarDec,
29
76
  varContext: varContextRef.current
30
77
  })));
78
+ // 如果启用了 scale-area,包装一个 GestureDetector
79
+ if (scaleArea && scaleGesture) {
80
+ movableComponent = createElement(MovableAreaContext.Provider, { value: contextValue }, createElement(GestureDetector, { gesture: scaleGesture }, createElement(View, innerProps, wrapChildren(props, {
81
+ hasVarDec,
82
+ varContext: varContextRef.current
83
+ }))));
84
+ }
31
85
  if (hasPositionFixed) {
32
86
  movableComponent = createElement(Portal, null, movableComponent);
33
87
  }
@@ -7,13 +7,13 @@
7
7
  * ✘ damping
8
8
  * ✘ friction
9
9
  * ✔ disabled
10
- * scale
11
- * scale-min
12
- * scale-max
13
- * scale-value
10
+ * scale
11
+ * scale-min
12
+ * scale-max
13
+ * scale-value
14
14
  * ✔ animation
15
15
  * ✔ bindchange
16
- * bindscale
16
+ * bindscale
17
17
  * ✔ htouchmove
18
18
  * ✔ vtouchmove
19
19
  */
@@ -24,7 +24,7 @@ import useNodesRef from './useNodesRef';
24
24
  import { MovableAreaContext } from './context';
25
25
  import { useTransformStyle, splitProps, splitStyle, HIDDEN_STYLE, wrapChildren, flatGesture, extendObject, omit, useNavigation } from './utils';
26
26
  import { GestureDetector, Gesture } from 'react-native-gesture-handler';
27
- import Animated, { useSharedValue, useAnimatedStyle, withDecay, runOnJS, runOnUI, withSpring } from 'react-native-reanimated';
27
+ import Animated, { useSharedValue, useAnimatedStyle, withDecay, runOnJS, runOnUI, withSpring, withTiming } from 'react-native-reanimated';
28
28
  import { collectDataset, noop } from '@mpxjs/utils';
29
29
  const styles = StyleSheet.create({
30
30
  container: {
@@ -41,8 +41,8 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
41
41
  const hasLayoutRef = useRef(false);
42
42
  const propsRef = useRef({});
43
43
  propsRef.current = (props || {});
44
- const { x = 0, y = 0, inertia = false, disabled = false, animation = true, 'out-of-bounds': outOfBounds = false, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, direction = 'none', 'simultaneous-handlers': originSimultaneousHandlers = [], 'wait-for': waitFor = [], style = {}, changeThrottleTime = 60, bindtouchstart, catchtouchstart, bindhtouchmove, bindvtouchmove, bindtouchmove, catchhtouchmove, catchvtouchmove, catchtouchmove, bindtouchend, catchtouchend, bindchange } = props;
45
- const { hasSelfPercent, normalStyle, hasVarDec, varContextRef, setWidth, setHeight } = useTransformStyle(Object.assign({}, style, styles.container), { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
44
+ const { x = 0, y = 0, inertia = false, disabled = false, animation = true, scale = false, 'scale-min': scaleMin = 0.1, 'scale-max': scaleMax = 10, 'scale-value': scaleValue = 1, 'out-of-bounds': outOfBounds = false, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, direction = 'none', 'disable-event-passthrough': disableEventPassthrough = false, 'simultaneous-handlers': originSimultaneousHandlers = [], 'wait-for': waitFor = [], style = {}, changeThrottleTime = 60, bindtouchstart, catchtouchstart, bindhtouchmove, bindvtouchmove, bindtouchmove, catchhtouchmove, catchvtouchmove, catchtouchmove, bindtouchend, catchtouchend, bindscale, bindchange, onLayout: propsOnLayout } = props;
45
+ const { hasSelfPercent, normalStyle, hasVarDec, varContextRef, setWidth, setHeight } = useTransformStyle(Object.assign({}, styles.container, style), { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
46
46
  const navigation = useNavigation();
47
47
  const prevSimultaneousHandlersRef = useRef(originSimultaneousHandlers || []);
48
48
  const prevWaitForHandlersRef = useRef(waitFor || []);
@@ -50,6 +50,8 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
50
50
  const { textStyle, innerStyle } = splitStyle(normalStyle);
51
51
  const offsetX = useSharedValue(x);
52
52
  const offsetY = useSharedValue(y);
53
+ const currentScale = useSharedValue(1);
54
+ const layoutValue = useSharedValue({});
53
55
  const startPosition = useSharedValue({
54
56
  x: 0,
55
57
  y: 0
@@ -100,6 +102,38 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
100
102
  layoutRef
101
103
  }, propsRef.current));
102
104
  }, []);
105
+ const handleTriggerScale = useCallback(({ x, y, scale }) => {
106
+ const { bindscale } = propsRef.current;
107
+ if (!bindscale)
108
+ return;
109
+ bindscale(getCustomEvent('scale', {}, {
110
+ detail: {
111
+ x,
112
+ y,
113
+ scale
114
+ },
115
+ layoutRef
116
+ }, propsRef.current));
117
+ }, []);
118
+ const checkBoundaryPosition = useCallback(({ positionX, positionY }) => {
119
+ 'worklet';
120
+ let x = positionX;
121
+ let y = positionY;
122
+ // 计算边界限制
123
+ if (x > draggableXRange.value[1]) {
124
+ x = draggableXRange.value[1];
125
+ }
126
+ else if (x < draggableXRange.value[0]) {
127
+ x = draggableXRange.value[0];
128
+ }
129
+ if (y > draggableYRange.value[1]) {
130
+ y = draggableYRange.value[1];
131
+ }
132
+ else if (y < draggableYRange.value[0]) {
133
+ y = draggableYRange.value[0];
134
+ }
135
+ return { x, y };
136
+ }, []);
103
137
  // 节流版本的 change 事件触发
104
138
  const handleTriggerChangeThrottled = useCallback(({ x, y, type }) => {
105
139
  'worklet';
@@ -139,12 +173,193 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
139
173
  }
140
174
  })();
141
175
  }, [x, y]);
142
- useEffect(() => {
143
- const { width, height } = layoutRef.current;
144
- if (width && height) {
145
- resetBoundaryAndCheck({ width, height });
176
+ // 提取通用的缩放边界计算函数
177
+ const calculateScaleBoundaryPosition = useCallback(({ currentOffsetX, currentOffsetY, newScale, width, height }) => {
178
+ 'worklet';
179
+ const prevScale = currentScale.value;
180
+ // 计算元素当前中心点(在屏幕上的位置)
181
+ const currentCenterX = currentOffsetX + (width * prevScale) / 2;
182
+ const currentCenterY = currentOffsetY + (height * prevScale) / 2;
183
+ // 实现中心缩放:保持元素中心点不变
184
+ // 计算缩放后为了保持中心点不变需要的新offset位置
185
+ let newOffsetX = currentCenterX - (width * newScale) / 2;
186
+ let newOffsetY = currentCenterY - (height * newScale) / 2;
187
+ // 缩放过程中实时边界检测
188
+ // 计算新的边界范围
189
+ const top = (style.position === 'absolute' && style.top) || 0;
190
+ const left = (style.position === 'absolute' && style.left) || 0;
191
+ const scaledWidth = width * newScale;
192
+ const scaledHeight = height * newScale;
193
+ // 计算新缩放值下的边界限制
194
+ const maxOffsetY = MovableAreaLayout.height - scaledHeight - top;
195
+ const maxOffsetX = MovableAreaLayout.width - scaledWidth - left;
196
+ let xMin, xMax, yMin, yMax;
197
+ if (MovableAreaLayout.width < scaledWidth) {
198
+ xMin = maxOffsetX;
199
+ xMax = -left;
200
+ }
201
+ else {
202
+ xMin = -left;
203
+ xMax = maxOffsetX < 0 ? -left : maxOffsetX;
204
+ }
205
+ if (MovableAreaLayout.height < scaledHeight) {
206
+ yMin = maxOffsetY;
207
+ yMax = -top;
208
+ }
209
+ else {
210
+ yMin = -top;
211
+ yMax = maxOffsetY < 0 ? -top : maxOffsetY;
212
+ }
213
+ // 应用边界限制
214
+ if (newOffsetX > xMax) {
215
+ newOffsetX = xMax;
216
+ }
217
+ else if (newOffsetX < xMin) {
218
+ newOffsetX = xMin;
219
+ }
220
+ if (newOffsetY > yMax) {
221
+ newOffsetY = yMax;
222
+ }
223
+ else if (newOffsetY < yMin) {
224
+ newOffsetY = yMin;
225
+ }
226
+ return { x: newOffsetX, y: newOffsetY };
227
+ }, [MovableAreaLayout.height, MovableAreaLayout.width, style.position, style.top, style.left]);
228
+ // 提取通用的缩放处理函数
229
+ const handleScaleUpdate = useCallback((scaleInfo) => {
230
+ 'worklet';
231
+ if (disabled)
232
+ return;
233
+ // 判断缩放方向并计算新的缩放值
234
+ const isZoomingIn = scaleInfo.scale > 1;
235
+ const isZoomingOut = scaleInfo.scale < 1;
236
+ let newScale;
237
+ if (isZoomingIn) {
238
+ // 放大:增加缩放值
239
+ newScale = currentScale.value + (scaleInfo.scale - 1) * 0.5;
240
+ }
241
+ else if (isZoomingOut) {
242
+ // 缩小:减少缩放值
243
+ newScale = currentScale.value - (1 - scaleInfo.scale) * 0.5;
244
+ }
245
+ else {
246
+ // 没有缩放变化
247
+ newScale = currentScale.value;
248
+ }
249
+ // 限制缩放值在 scaleMin 和 scaleMax 之间
250
+ newScale = Math.max(scaleMin, Math.min(scaleMax, newScale));
251
+ // 只有当缩放值真正改变时才调整位置
252
+ if (Math.abs(newScale - currentScale.value) > 0.01) {
253
+ // 获取元素尺寸
254
+ const { width = 0, height = 0 } = layoutValue.value;
255
+ if (width > 0 && height > 0) {
256
+ // 使用通用的边界计算函数
257
+ const { x: newOffsetX, y: newOffsetY } = calculateScaleBoundaryPosition({
258
+ currentOffsetX: offsetX.value,
259
+ currentOffsetY: offsetY.value,
260
+ newScale,
261
+ width,
262
+ height
263
+ });
264
+ offsetX.value = newOffsetX;
265
+ offsetY.value = newOffsetY;
266
+ // 更新缩放值
267
+ currentScale.value = newScale;
268
+ }
146
269
  }
270
+ else {
271
+ currentScale.value = newScale;
272
+ }
273
+ if (bindscale) {
274
+ runOnJS(handleTriggerScale)({
275
+ x: offsetX.value,
276
+ y: offsetY.value,
277
+ scale: newScale
278
+ });
279
+ }
280
+ }, [disabled, scaleMin, scaleMax, bindscale, handleTriggerScale, calculateScaleBoundaryPosition, style.position, style.top, style.left, MovableAreaLayout.height, MovableAreaLayout.width]);
281
+ useEffect(() => {
282
+ runOnUI(() => {
283
+ if (currentScale.value !== scaleValue) {
284
+ // 限制缩放值在 scaleMin 和 scaleMax 之间
285
+ const clampedScale = Math.max(scaleMin, Math.min(scaleMax, scaleValue));
286
+ // 实现中心缩放的位置补偿
287
+ const { width = 0, height = 0 } = layoutValue.value;
288
+ if (width > 0 && height > 0) {
289
+ // 使用通用的边界计算函数
290
+ const { x: newOffsetX, y: newOffsetY } = calculateScaleBoundaryPosition({
291
+ currentOffsetX: offsetX.value,
292
+ currentOffsetY: offsetY.value,
293
+ newScale: clampedScale,
294
+ width,
295
+ height
296
+ });
297
+ // 同时更新缩放值和位置
298
+ if (animation) {
299
+ currentScale.value = withTiming(clampedScale, {
300
+ duration: 1000
301
+ }, () => {
302
+ handleRestBoundaryAndCheck();
303
+ });
304
+ offsetX.value = withTiming(newOffsetX, { duration: 1000 });
305
+ offsetY.value = withTiming(newOffsetY, { duration: 1000 });
306
+ }
307
+ else {
308
+ currentScale.value = clampedScale;
309
+ offsetX.value = newOffsetX;
310
+ offsetY.value = newOffsetY;
311
+ handleRestBoundaryAndCheck();
312
+ }
313
+ }
314
+ else {
315
+ // 如果还没有尺寸信息,只更新缩放值
316
+ if (animation) {
317
+ currentScale.value = withTiming(clampedScale, {
318
+ duration: 1000
319
+ }, () => {
320
+ handleRestBoundaryAndCheck();
321
+ });
322
+ }
323
+ else {
324
+ currentScale.value = clampedScale;
325
+ handleRestBoundaryAndCheck();
326
+ }
327
+ }
328
+ if (bindscale) {
329
+ runOnJS(handleTriggerScale)({
330
+ x: offsetX.value,
331
+ y: offsetY.value,
332
+ scale: clampedScale
333
+ });
334
+ }
335
+ }
336
+ })();
337
+ }, [scaleValue, scaleMin, scaleMax, animation]);
338
+ useEffect(() => {
339
+ runOnUI(handleRestBoundaryAndCheck)();
147
340
  }, [MovableAreaLayout.height, MovableAreaLayout.width]);
341
+ // 生成唯一 ID
342
+ const viewId = useMemo(() => `movable-view-${Date.now()}-${Math.random()}`, []);
343
+ // 注册到 MovableArea(如果启用了 scale-area)
344
+ useEffect(() => {
345
+ if (MovableAreaLayout.scaleArea && MovableAreaLayout.registerMovableView && MovableAreaLayout.unregisterMovableView) {
346
+ const handleAreaScale = (scaleInfo) => {
347
+ 'worklet';
348
+ handleScaleUpdate({ scale: scaleInfo.scale });
349
+ };
350
+ const handleAreaScaleEnd = () => {
351
+ 'worklet';
352
+ handleRestBoundaryAndCheck();
353
+ };
354
+ MovableAreaLayout.registerMovableView?.(viewId, {
355
+ onScale: scale ? handleAreaScale : noop,
356
+ onScaleEnd: scale ? handleAreaScaleEnd : noop
357
+ });
358
+ return () => {
359
+ MovableAreaLayout.unregisterMovableView?.(viewId);
360
+ };
361
+ }
362
+ }, [MovableAreaLayout.scaleArea, viewId, scale, handleScaleUpdate]);
148
363
  const getTouchSource = useCallback((offsetX, offsetY) => {
149
364
  const hasOverBoundary = offsetX < draggableXRange.value[0] || offsetX > draggableXRange.value[1] ||
150
365
  offsetY < draggableYRange.value[0] || offsetY > draggableYRange.value[1];
@@ -169,61 +384,45 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
169
384
  return source;
170
385
  }, []);
171
386
  const setBoundary = useCallback(({ width, height }) => {
387
+ 'worklet';
172
388
  const top = (style.position === 'absolute' && style.top) || 0;
173
389
  const left = (style.position === 'absolute' && style.left) || 0;
174
- const scaledWidth = width || 0;
175
- const scaledHeight = height || 0;
176
- const maxY = MovableAreaLayout.height - scaledHeight - top;
177
- const maxX = MovableAreaLayout.width - scaledWidth - left;
390
+ // 使用左上角缩放,计算offset位置的边界范围
391
+ const currentScaleVal = currentScale.value;
392
+ const scaledWidth = (width || 0) * currentScaleVal;
393
+ const scaledHeight = (height || 0) * currentScaleVal;
394
+ // offset位置的边界:左上角可以移动的范围
395
+ const maxOffsetY = MovableAreaLayout.height - scaledHeight - top;
396
+ const maxOffsetX = MovableAreaLayout.width - scaledWidth - left;
178
397
  let xRange;
179
398
  let yRange;
180
399
  if (MovableAreaLayout.width < scaledWidth) {
181
- xRange = [maxX, 0];
400
+ xRange = [maxOffsetX, -left];
182
401
  }
183
402
  else {
184
- xRange = [left === 0 ? 0 : -left, maxX < 0 ? 0 : maxX];
403
+ xRange = [-left, maxOffsetX < 0 ? -left : maxOffsetX];
185
404
  }
186
405
  if (MovableAreaLayout.height < scaledHeight) {
187
- yRange = [maxY, 0];
406
+ yRange = [maxOffsetY, -top];
188
407
  }
189
408
  else {
190
- yRange = [top === 0 ? 0 : -top, maxY < 0 ? 0 : maxY];
409
+ yRange = [-top, maxOffsetY < 0 ? -top : maxOffsetY];
191
410
  }
192
411
  draggableXRange.value = xRange;
193
412
  draggableYRange.value = yRange;
194
413
  }, [MovableAreaLayout.height, MovableAreaLayout.width, style.position, style.top, style.left]);
195
- const checkBoundaryPosition = useCallback(({ positionX, positionY }) => {
414
+ const resetBoundaryAndCheck = ({ width, height }) => {
196
415
  'worklet';
197
- let x = positionX;
198
- let y = positionY;
199
- // 计算边界限制
200
- if (x > draggableXRange.value[1]) {
201
- x = draggableXRange.value[1];
202
- }
203
- else if (x < draggableXRange.value[0]) {
204
- x = draggableXRange.value[0];
205
- }
206
- if (y > draggableYRange.value[1]) {
207
- y = draggableYRange.value[1];
416
+ setBoundary({ width, height });
417
+ const positionX = offsetX.value;
418
+ const positionY = offsetY.value;
419
+ const { x: newX, y: newY } = checkBoundaryPosition({ positionX, positionY });
420
+ if (positionX !== newX) {
421
+ offsetX.value = newX;
208
422
  }
209
- else if (y < draggableYRange.value[0]) {
210
- y = draggableYRange.value[0];
423
+ if (positionY !== newY) {
424
+ offsetY.value = newY;
211
425
  }
212
- return { x, y };
213
- }, []);
214
- const resetBoundaryAndCheck = ({ width, height }) => {
215
- setBoundary({ width, height });
216
- runOnUI(() => {
217
- const positionX = offsetX.value;
218
- const positionY = offsetY.value;
219
- const { x: newX, y: newY } = checkBoundaryPosition({ positionX, positionY });
220
- if (positionX !== newX) {
221
- offsetX.value = newX;
222
- }
223
- if (positionY !== newY) {
224
- offsetY.value = newY;
225
- }
226
- })();
227
426
  };
228
427
  const onLayout = (e) => {
229
428
  hasLayoutRef.current = true;
@@ -235,13 +434,18 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
235
434
  nodeRef.current?.measure((x, y, width, height) => {
236
435
  const { y: navigationY = 0 } = navigation?.layout || {};
237
436
  layoutRef.current = { x, y: y - navigationY, width, height, offsetLeft: 0, offsetTop: 0 };
238
- resetBoundaryAndCheck({ width, height });
437
+ // 同时更新 layoutValue,供缩放逻辑使用
438
+ runOnUI(() => {
439
+ layoutValue.value = { width, height };
440
+ resetBoundaryAndCheck({ width: width, height: height });
441
+ })();
239
442
  });
240
- props.onLayout && props.onLayout(e);
443
+ propsOnLayout && propsOnLayout(e);
241
444
  };
242
445
  const extendEvent = useCallback((e, type) => {
243
446
  const { y: navigationY = 0 } = navigation?.layout || {};
244
447
  const touchArr = [e.changedTouches, e.allTouches];
448
+ const currentProps = propsRef.current;
245
449
  touchArr.forEach(touches => {
246
450
  touches && touches.forEach((item) => {
247
451
  item.pageX = item.absoluteX;
@@ -253,8 +457,8 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
253
457
  Object.assign(e, {
254
458
  touches: type === 'end' ? [] : e.allTouches,
255
459
  currentTarget: {
256
- id: props.id || '',
257
- dataset: collectDataset(props),
460
+ id: currentProps.id || '',
461
+ dataset: collectDataset(currentProps),
258
462
  offsetLeft: 0,
259
463
  offsetTop: 0
260
464
  },
@@ -295,6 +499,13 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
295
499
  bindtouchend && bindtouchend(e);
296
500
  catchtouchend && catchtouchend(e);
297
501
  };
502
+ const handleRestBoundaryAndCheck = () => {
503
+ 'worklet';
504
+ const { width, height } = layoutValue.value;
505
+ if (width && height) {
506
+ resetBoundaryAndCheck({ width, height });
507
+ }
508
+ };
298
509
  const gesture = useMemo(() => {
299
510
  const handleTriggerMove = (e) => {
300
511
  'worklet';
@@ -310,6 +521,8 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
310
521
  }
311
522
  };
312
523
  const gesturePan = Gesture.Pan()
524
+ .minPointers(1)
525
+ .maxPointers(1)
313
526
  .onTouchesDown((e) => {
314
527
  'worklet';
315
528
  const changedTouches = e.changedTouches[0] || { x: 0, y: 0 };
@@ -449,11 +662,13 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
449
662
  }
450
663
  })
451
664
  .withRef(movableGestureRef);
452
- if (direction === 'horizontal') {
453
- gesturePan.activeOffsetX([-5, 5]).failOffsetY([-5, 5]);
454
- }
455
- else if (direction === 'vertical') {
456
- gesturePan.activeOffsetY([-5, 5]).failOffsetX([-5, 5]);
665
+ if (!disableEventPassthrough) {
666
+ if (direction === 'horizontal') {
667
+ gesturePan.activeOffsetX([-5, 5]).failOffsetY([-5, 5]);
668
+ }
669
+ else if (direction === 'vertical') {
670
+ gesturePan.activeOffsetY([-5, 5]).failOffsetX([-5, 5]);
671
+ }
457
672
  }
458
673
  if (simultaneousHandlers && simultaneousHandlers.length) {
459
674
  gesturePan.simultaneousWithExternalGesture(...simultaneousHandlers);
@@ -461,13 +676,43 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
461
676
  if (waitForHandlers && waitForHandlers.length) {
462
677
  gesturePan.requireExternalGestureToFail(...waitForHandlers);
463
678
  }
679
+ // 添加缩放手势支持
680
+ if (scale && !MovableAreaLayout.scaleArea) {
681
+ const gesturePinch = Gesture.Pinch()
682
+ .onUpdate((e) => {
683
+ 'worklet';
684
+ handleScaleUpdate({ scale: e.scale });
685
+ })
686
+ .onEnd((e) => {
687
+ 'worklet';
688
+ if (disabled)
689
+ return;
690
+ // 确保最终缩放值在有效范围内
691
+ const finalScale = Math.max(scaleMin, Math.min(scaleMax, currentScale.value));
692
+ if (finalScale !== currentScale.value) {
693
+ currentScale.value = finalScale;
694
+ if (bindscale) {
695
+ runOnJS(handleTriggerScale)({
696
+ x: offsetX.value,
697
+ y: offsetY.value,
698
+ scale: finalScale
699
+ });
700
+ }
701
+ }
702
+ // 缩放结束后重新检查边界
703
+ handleRestBoundaryAndCheck();
704
+ });
705
+ // 根据手指数量自动区分手势:一指移动,两指缩放
706
+ return Gesture.Exclusive(gesturePan, gesturePinch);
707
+ }
464
708
  return gesturePan;
465
- }, [disabled, direction, inertia, outOfBounds, gestureSwitch.current]);
709
+ }, [disabled, direction, inertia, outOfBounds, scale, scaleMin, scaleMax, animation, gestureSwitch.current, handleScaleUpdate, MovableAreaLayout.scaleArea]);
466
710
  const animatedStyles = useAnimatedStyle(() => {
467
711
  return {
468
712
  transform: [
469
713
  { translateX: offsetX.value },
470
- { translateY: offsetY.value }
714
+ { translateY: offsetY.value },
715
+ { scale: currentScale.value }
471
716
  ]
472
717
  };
473
718
  });
@@ -504,7 +749,7 @@ const _MovableView = forwardRef((movableViewProps, ref) => {
504
749
  const innerProps = useInnerProps(extendObject({}, filterProps, {
505
750
  ref: nodeRef,
506
751
  onLayout: onLayout,
507
- style: [innerStyle, animatedStyles, layoutStyle]
752
+ style: [{ transformOrigin: 'top left' }, innerStyle, animatedStyles, layoutStyle]
508
753
  }, rewriteCatchEvent()));
509
754
  return createElement(GestureDetector, { gesture: gesture }, createElement(Animated.View, innerProps, wrapChildren(props, {
510
755
  hasVarDec,