@ray-js/ipc-player-integration 0.0.50-beta.1 → 0.0.50-beta.3

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.
Files changed (39) hide show
  1. package/README.md +11 -11
  2. package/lib/ctx/ctx.composition.js +5 -5
  3. package/lib/hooks/useBattery/index.js +7 -1
  4. package/lib/iconfont/iconfont.css +127 -7
  5. package/lib/iconfont/iconfont.js +10 -10
  6. package/lib/iconfont/iconfont.json +234 -24
  7. package/lib/iconfont/iconfont.ttf +0 -0
  8. package/lib/iconfont/iconfont.woff +0 -0
  9. package/lib/iconfont/iconfont.woff2 +0 -0
  10. package/lib/interface.d.ts +1 -1
  11. package/lib/interface.js +1 -1
  12. package/lib/ports.output.d.ts +13 -0
  13. package/lib/ports.output.js +13 -0
  14. package/lib/ui/bottomLeftContent.js +141 -27
  15. package/lib/ui/bottomRightContent.d.ts +6 -1
  16. package/lib/ui/bottomRightContent.js +43 -5
  17. package/lib/ui/ui.js +101 -21
  18. package/lib/ui/ui.less +17 -0
  19. package/lib/widgets/battery/battery.js +11 -3
  20. package/lib/widgets/battery/battery.less +11 -0
  21. package/lib/widgets/battery/batteryFull.js +11 -3
  22. package/lib/widgets/voiceIntercom/voiceIntercom.js +6 -1
  23. package/lib/widgets/zoomTurntable/constants.d.ts +187 -32
  24. package/lib/widgets/zoomTurntable/constants.js +306 -45
  25. package/lib/widgets/zoomTurntable/measureVisible.d.ts +9 -0
  26. package/lib/widgets/zoomTurntable/measureVisible.js +10 -0
  27. package/lib/widgets/zoomTurntable/turntable/index.js +9 -1
  28. package/lib/widgets/zoomTurntable/turntable/rjs/index.js +27 -4
  29. package/lib/widgets/zoomTurntable/turntable/rjs/index.rjs +265 -36
  30. package/lib/widgets/zoomTurntable/turntable/rjs/index.tyml +1 -0
  31. package/lib/widgets/zoomTurntable/turntable/type.d.ts +41 -2
  32. package/lib/widgets/zoomTurntable/useLensZoom.d.ts +3 -0
  33. package/lib/widgets/zoomTurntable/useLensZoom.js +96 -4
  34. package/lib/widgets/zoomTurntable/zoomTurntableDock.js +143 -13
  35. package/lib/widgets/zoomTurntable/zoomTurntableEntry.js +116 -23
  36. package/lib/widgets/zoomTurntable/zoomTurntableEntry.less +11 -1
  37. package/lib/widgets/zoomTurntable/zoomTurntableTip.js +34 -22
  38. package/lib/widgets/zoomTurntable/zoomTurntableTip.less +23 -7
  39. package/package.json +2 -2
@@ -5,8 +5,8 @@ import { PlayState } from '../../interface';
5
5
  import { useDpState } from '../../hooks/useDpState';
6
6
  import { setScaleMultipleEvent, zoomLevelChange } from '../../ui/constant';
7
7
  import { MOCK_ZOOM_SKILL, MULTI_ZOOM_DP_CODE, dumpChannelApis, isChannelChangeSupported, isZoomSkillValid, lensCount, parseZoomSkill, queryCurrentLens, showSmallMonitor, subscribeChannelChange, switchLens } from './lensSkill';
8
- import { DEFAULT_STEP, D_MIN, buildMarks, buildTickValues, indexOfLensId, matchLens, maxScaleMultiple, toDisplayZoom, toScaleMultiple, zoomBounds } from './lensModel';
9
- import { CHANNEL_SETTLE_TIMEOUT, D_MATCH_EPS, GESTURE_SWITCH_DEBOUNCE, SWITCH_LENS_TIMEOUT, zoomTurntableLensChange, zoomTurntableZoomChange } from './constants';
8
+ import { DEFAULT_STEP, D_MIN, buildMarks, buildTickValues, indexOfLensId, lensRange, matchLens, maxScaleMultiple, toDisplayZoom, toScaleMultiple, zoomBounds } from './lensModel';
9
+ import { CHANNEL_SETTLE_TIMEOUT, D_MATCH_EPS, GESTURE_SWITCH_DEBOUNCE, SWITCH_LENS_TIMEOUT, ZOOM_PREVIEW_THROTTLE, zoomTurntableLensChange, zoomTurntableSwitching, zoomTurntableZoomChange } from './constants';
10
10
  /**
11
11
  * 内部调试开关:置 true 时用 MOCK_ZOOM_SKILL 顶替 ipc_multi_zoom 的上报值。
12
12
  *
@@ -89,15 +89,43 @@ export const useLensZoom = options => {
89
89
 
90
90
  /** 画面绝对倍率;挂载时按已记录的 d 恢复读数 */
91
91
  const [curM, setCurM] = useState(() => zoomSkill ? toDisplayZoom(zoomSkill, zoomLensIndex, getLastD(devId)) : D_MIN);
92
+ /**
93
+ * 正在切往的目标镜头下标(-1 表示没有切换进行中)。
94
+ * 切换真空期里镜头下标还没动,倍率 tab 要靠它把高亮提前落到目标镜头,
95
+ * 免得原镜头的 tab 上冒出目标镜头的数值。
96
+ */
97
+ const [switchingIndex, setSwitchingIndex] = useState(-1);
92
98
  /** 拖动中:屏蔽回读,避免指针被拽回 */
93
99
  const draggingRef = useRef(false);
94
100
  /** 手势缩放跨临界点判定的 trailing 防抖 */
95
101
  const gestureTimerRef = useRef();
102
+ /** 拖动中连续下发的节流时间戳 */
103
+ const previewAtRef = useRef(0);
96
104
  const lensIndexRef = useRef(zoomLensIndex);
97
105
  lensIndexRef.current = zoomLensIndex;
98
106
  const skillRef = useRef(zoomSkill);
99
107
  skillRef.current = zoomSkill;
100
108
 
109
+ /** 标记 / 清除「切换进行中」,并广播给其它 hook 实例 */
110
+ const markSwitching = useMemoizedFn(index => {
111
+ setSwitchingIndex(index);
112
+ event.emit(zoomTurntableSwitching, {
113
+ index
114
+ });
115
+ });
116
+ useEffect(() => {
117
+ const onSwitching = _ref => {
118
+ let {
119
+ index
120
+ } = _ref;
121
+ return setSwitchingIndex(index);
122
+ };
123
+ event.on(zoomTurntableSwitching, onSwitching);
124
+ return () => {
125
+ event.off(zoomTurntableSwitching, onSwitching);
126
+ };
127
+ }, [event]);
128
+
101
129
  // ---------------- 能力加载 ----------------
102
130
  useEffect(() => {
103
131
  if (skipLoad || !devId || !showZoomTurntable) return undefined;
@@ -181,9 +209,37 @@ export const useLensZoom = options => {
181
209
  }, [devId, multiLensReady, playState, skipLoad, syncToLens, syncSmallMonitorByD]);
182
210
 
183
211
  // ---------------- 下发:curM → 切镜头 + d ----------------
212
+
213
+ /**
214
+ * 拖动过程中的连续下发:画面跟着转盘实时变倍,松手才落地的话中间是"死"的。
215
+ *
216
+ * 只在当前镜头区间内下发本地数字缩放 d,超出区间的部分夹到边界 ——
217
+ * 跨镜头要 switchChannel,逐帧下发扛不住,留给松手后的 applyZoom。
218
+ */
219
+ const previewZoom = useMemoizedFn(targetM => {
220
+ const skill = skillRef.current;
221
+ if (!skill) return;
222
+ const now = Date.now();
223
+ if (now - previewAtRef.current < ZOOM_PREVIEW_THROTTLE) return;
224
+ previewAtRef.current = now;
225
+ const index = lensIndexRef.current;
226
+ const [lo, hi] = lensRange(skill, index);
227
+ const clamped = Math.min(hi, Math.max(lo, targetM));
228
+ const d = toScaleMultiple(skill, index, clamped);
229
+ setLastD(devId, d);
230
+ // immediate:连续拖动时属性本来就在变,不需要 ui.tsx 那套"先对齐再设目标"的中间态,
231
+ // 走对齐反而会插入一帧 actual 值,画面看起来会抖
232
+ event.emit(setScaleMultipleEvent, {
233
+ value: d,
234
+ immediate: true
235
+ });
236
+ // 小窗按下发值切换,内部按 devId 去重,只在跨过 d=1 时真正调一次
237
+ syncSmallMonitorByD(d);
238
+ });
184
239
  const beginDrag = useMemoizedFn(value => {
185
240
  draggingRef.current = true;
186
241
  setCurM(value);
242
+ previewZoom(value);
187
243
  });
188
244
 
189
245
  /**
@@ -198,6 +254,7 @@ export const useLensZoom = options => {
198
254
  console.log('[scaleMultiple] zoomTurntable emit setScaleMultipleEvent', D_MIN, `lensIndex=${index}`, `lastD=${getLastD(devId)}`);
199
255
  event.emit(setScaleMultipleEvent, D_MIN);
200
256
  syncToLens(index, D_MIN);
257
+ markSwitching(-1);
201
258
  // 切镜头即回到该镜头的光学正倍数,小窗关掉(onChannelChange / 超时兜底都会走到这里)
202
259
  applySmallMonitor(false);
203
260
  event.emit(zoomTurntableLensChange, {
@@ -236,6 +293,9 @@ export const useLensZoom = options => {
236
293
  // 切物理镜头:从下发到 channelChange 回来这段屏蔽回读
237
294
  console.log('[zoomTurntable] 切换镜头', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `targetM=${targetM.toFixed(2)}`);
238
295
  takePendingSwitch(devId); // 上一次没结算完的直接作废,以最新这次为准
296
+ // 高亮先落到目标镜头:这段真空期里 lensIndex 还是原镜头,
297
+ // 不提前挪的话原 tab 会显示目标镜头的读数,看起来就是数值跳变
298
+ markSwitching(nextIndex);
239
299
  switchSeq += 1;
240
300
  const seq = switchSeq;
241
301
  /** 读数退回「当前镜头 + 已记录的 d」;切没切成不确定时才用 */
@@ -247,6 +307,7 @@ export const useLensZoom = options => {
247
307
  index: nextIndex,
248
308
  timer: setTimeout(() => {
249
309
  pendingSwitchStore.delete(devId);
310
+ markSwitching(-1);
250
311
  rollback();
251
312
  console.warn('[zoomTurntable] switchChannel 无回调,读数已回滚', `${prevIndex} -> ${nextIndex}`, `${SWITCH_LENS_TIMEOUT}ms`);
252
313
  }, SWITCH_LENS_TIMEOUT)
@@ -257,6 +318,7 @@ export const useLensZoom = options => {
257
318
 
258
319
  if (!ok) {
259
320
  takePendingSwitch(devId);
321
+ markSwitching(-1);
260
322
  console.warn('[zoomTurntable] 切换镜头失败,读数已回滚', `${prevIndex} -> ${nextIndex}`);
261
323
  setCurM(toDisplayZoom(skill, prevIndex, getLastD(devId)));
262
324
  return;
@@ -287,6 +349,18 @@ export const useLensZoom = options => {
287
349
  }, CHANNEL_SETTLE_TIMEOUT);
288
350
  });
289
351
 
352
+ /**
353
+ * 拖动状态兜底复位。
354
+ *
355
+ * `beginDrag` 置上的 draggingRef 正常由 `applyZoom`(onChangeEnd)复位,但异常路径下
356
+ * onChangeEnd 可能永远不来 —— 拖动中转盘被收起(点画面 / 切横竖屏 / 停流)就是这种情况。
357
+ * draggingRef 只要还挂着 true,本实例后续所有 zoomLevelChange 都会被 early return 丢掉,
358
+ * 手势缩放的读数就再也不更新了。挂载方在收尾路径上调一次它即可。
359
+ */
360
+ const endDrag = useMemoizedFn(() => {
361
+ draggingRef.current = false;
362
+ });
363
+
290
364
  /** 拖动结束:按倍率匹配镜头后落地 */
291
365
  const applyZoom = useMemoizedFn(async targetM => {
292
366
  draggingRef.current = false;
@@ -310,10 +384,10 @@ export const useLensZoom = options => {
310
384
  * 切镜头的统一收尾入口:自己下发的(switchChannel)与外部来源(设备侧 / 其他端)都走这里。
311
385
  * 自己下发的那次不在 switchChannel 回调里收尾,就是为了等这条事件 —— 设备真的切过去了再动画面。
312
386
  */
313
- const onChannelChange = useMemoizedFn(_ref => {
387
+ const onChannelChange = useMemoizedFn(_ref2 => {
314
388
  let {
315
389
  channel
316
- } = _ref;
390
+ } = _ref2;
317
391
  const skill = skillRef.current;
318
392
  if (!skill) {
319
393
  console.warn('[zoomTurntable] 收到 channelChange 但镜头能力未就绪,已忽略', channel);
@@ -383,6 +457,21 @@ export const useLensZoom = options => {
383
457
  d,
384
458
  lensIndex: lensIndexRef.current
385
459
  });
460
+
461
+ // 画面已经顶到当前镜头区间的上界(也就是下一个镜头的光学倍数,如 1x 镜头放到 3.0):
462
+ // 这个边界值在归属上属于下一个镜头,必须立刻切过去。
463
+ // 走 trailing 防抖的话,那 500ms 里 tab 会挂着「下一镜头的值」显示在当前镜头上。
464
+ const [, upperBound] = lensRange(skill, lensIndexRef.current);
465
+ const nextIndex = matchLens(skill, next);
466
+ if (nextIndex !== lensIndexRef.current && next >= upperBound - D_MATCH_EPS) {
467
+ if (gestureTimerRef.current) {
468
+ clearTimeout(gestureTimerRef.current);
469
+ gestureTimerRef.current = undefined;
470
+ }
471
+ console.log('[zoomTurntable] 触到镜头区间上界,立即切换', `curM=${next.toFixed(2)}`, `${lensIndexRef.current} -> ${nextIndex}`);
472
+ applyToLens(nextIndex, next);
473
+ return;
474
+ }
386
475
  scheduleGestureLensSwitch(next);
387
476
  });
388
477
  useEffect(() => {
@@ -405,6 +494,8 @@ export const useLensZoom = options => {
405
494
  /** 镜头列表(按 value 升序),用于渲染倍率 tab */
406
495
  lenses: (_zoomSkill$lenses$sli = zoomSkill === null || zoomSkill === void 0 || (_zoomSkill$lenses = zoomSkill.lenses) === null || _zoomSkill$lenses === void 0 ? void 0 : _zoomSkill$lenses.slice(0, lensCount(zoomSkill))) !== null && _zoomSkill$lenses$sli !== void 0 ? _zoomSkill$lenses$sli : [],
407
496
  lensIndex: zoomLensIndex,
497
+ /** 正在切往的目标镜头下标;-1 表示没有切换进行中 */
498
+ switchingIndex,
408
499
  /** 画面绝对倍率 */
409
500
  curM,
410
501
  min,
@@ -416,6 +507,7 @@ export const useLensZoom = options => {
416
507
  ticks: zoomSkill ? buildTickValues(zoomSkill) : [],
417
508
  beginDrag,
418
509
  applyZoom,
510
+ endDrag,
419
511
  switchToLens
420
512
  };
421
513
  };
@@ -12,7 +12,7 @@ import { PlayState } from '../../interface';
12
12
  import { pauseTimeToHideAllComponent, playerTap } from '../../ui/constant';
13
13
  import { useLensZoom } from './useLensZoom';
14
14
  import ZoomTurntable from './turntable';
15
- import { CROP_RATIO_BOTTOM, CROP_RATIO_RIGHT, RELAYOUT_DELAYS, STYLE_OPTIONS_BOTTOM, STYLE_OPTIONS_RIGHT, VIBRATE_THROTTLE, zoomTurntableClose } from './constants';
15
+ import { CROP_RATIO_BOTTOM, CROP_RATIO_RIGHT, RELAYOUT_DELAYS, VIBRATE_FAST_STEPS, getStyleOptionsBottom, getVerticalPlayerHeight, getZoomLayoutDebugPx, getZoomVisibleHeightBottom, STYLE_OPTIONS_RIGHT, VIBRATE_THROTTLE, zoomTurntableClose } from './constants';
16
16
  import './zoomTurntableDock.less';
17
17
  /**
18
18
  * 当前页面 id,ty.nativeDisabled 需要它定位要禁用手势分发的页面。
@@ -40,17 +40,27 @@ const getPageId = () => {
40
40
  * 定位与层级由承载的 CoverView 通过 absolutePosition 提供。
41
41
  */
42
42
  export const ZoomTurntableDock = props => {
43
+ var _initialValueRef$curr;
43
44
  const {
44
45
  event,
45
46
  className
46
47
  } = props;
47
48
  const {
48
49
  screenType,
49
- playState
50
+ playState,
51
+ isVerticalFullLayout
50
52
  } = useStore({
51
53
  screenType: props.screenType,
52
- playState: props.playState
54
+ playState: props.playState,
55
+ isVerticalFullLayout: props.isVerticalFullLayout
53
56
  });
57
+
58
+ /**
59
+ * 竖屏全屏:底部工具条常驻,转盘停在它之上(canvas 底部不留触摸余量,否则会盖住按钮)。
60
+ * 普通竖屏:工具条已被 Entry 藏掉,转盘贴播放器底边(余量上下对称,底部那份沉到播放器外)。
61
+ * 判断必须与 Entry 的 absolutePosition 用同一套,否则 canvas 高度与 dock 位置对不上。
62
+ */
63
+ const isVerticalFull = screenType === 'vertical' && isVerticalFullLayout;
54
64
  const {
55
65
  multiLensReady,
56
66
  curM,
@@ -60,7 +70,8 @@ export const ZoomTurntableDock = props => {
60
70
  marks,
61
71
  ticks,
62
72
  beginDrag,
63
- applyZoom
73
+ applyZoom,
74
+ endDrag
64
75
  } = useLensZoom(_objectSpread(_objectSpread({}, props), {}, {
65
76
  skipLoad: true
66
77
  }));
@@ -73,6 +84,28 @@ export const ZoomTurntableDock = props => {
73
84
  /** 上次震动时间与上次震动对应的倍率,用于节流与跨刻度判定 */
74
85
  const lastVibrateAtRef = useRef(0);
75
86
  const lastVibrateValueRef = useRef(curM);
87
+
88
+ /**
89
+ * 转盘起始值:只在打开转盘 / 重建画布时取一次,作为首帧的 defaultValue。
90
+ *
91
+ * 取值时机要等镜头能力就绪:能力没就绪时 curM 只是 D_MIN 占位,
92
+ * 拿它当起点会让转盘停在错误刻度。
93
+ *
94
+ * 稳态读数由下面的受控 `value={curM}` 负责跟随(手势缩放画面时读数要跟着动)。
95
+ * 早先只传 defaultValue、完全不回写,是担心「回写在拖动中把指针拽走」——
96
+ * 而 rjs 的 setValue 本身就有拖动中 + 松手后 300ms 冷却期的双重忽略,这个顾虑不成立。
97
+ */
98
+ const initialValueRef = useRef(null);
99
+ const lastRemountRef = useRef(remountKey);
100
+ if (lastRemountRef.current !== remountKey) {
101
+ lastRemountRef.current = remountKey;
102
+ initialValueRef.current = null;
103
+ }
104
+ if (initialValueRef.current == null && multiLensReady && curM > 0) {
105
+ initialValueRef.current = curM;
106
+ }
107
+ // 兜底夹到 [min, max]:起点绝不能落在倍率域之外(曾出现过 0.0X)
108
+ const initialValue = Math.min(max, Math.max(min, (_initialValueRef$curr = initialValueRef.current) !== null && _initialValueRef$curr !== void 0 ? _initialValueRef$curr : min));
76
109
  const requestClose = useMemoizedFn(() => {
77
110
  event.emit(zoomTurntableClose);
78
111
  });
@@ -108,9 +141,64 @@ export const ZoomTurntableDock = props => {
108
141
  relayoutTimersRef.current = [];
109
142
  // 卸载兜底:手指还按着就被收起时,必须恢复原生手势
110
143
  setNativeDisabled(false);
144
+ // 拖动中被收起的话 onChangeEnd 不会来,draggingRef 会挂着 true 把后续的
145
+ // zoomLevelChange 全丢掉(手势缩放读数就不动了),这里补一次复位
146
+ endDrag();
111
147
  };
112
148
  }, []);
113
149
 
150
+ // 诊断:把浮层、canvas 与播放器容器的真实布局打出来,逐项核对两类历史坑:
151
+ // 1. dock 实测高 ≈ 期望_dock高 → absolutePosition 的 rpx 换算对了
152
+ // (曾经给 194 只量到 101px,dock 只有 canvas 一半高,canvas 顶部被裁、收不到 touch)
153
+ // 2. canvas 实测高 ≈ 期望_canvas高 → rjs/index.js 的 computeCanvasViewport 读到了 ellipseRx/Ry
154
+ // (曾经它硬编码 258×175.5,DOM 208 高、绘制 186 高,画面被拉伸且顶部溢出被裁)
155
+ // canvas顶溢出 > 0 就是「手势不生效」的直接信号:溢出 dock 的那部分收不到 touch。
156
+ // canvas底距播底 应 ≈ 48(让出底部工具条):小于它就说明 canvas 压在按钮上、会吃掉按钮点击。
157
+ useEffect(() => {
158
+ const timer = setTimeout(() => {
159
+ const query = ty.createSelectorQuery();
160
+ query.select('.ipc-player-plugin-zoom-turntable-dock').boundingClientRect();
161
+ query.select('.ipc-zoom-turntable-canvas').boundingClientRect();
162
+ query.select('.ipc-player-content').boundingClientRect();
163
+ query.exec(res => {
164
+ const dock = res === null || res === void 0 ? void 0 : res[0];
165
+ const canvas = res === null || res === void 0 ? void 0 : res[1];
166
+ const player = res === null || res === void 0 ? void 0 : res[2];
167
+ const visibleH = getZoomVisibleHeightBottom();
168
+ const expect = getZoomLayoutDebugPx(isVerticalFull);
169
+ console.log('[zoomTurntable] 浮层布局诊断', JSON.stringify({
170
+ dock: dock && {
171
+ top: dock.top,
172
+ w: dock.width,
173
+ h: dock.height
174
+ },
175
+ canvas: canvas && {
176
+ top: canvas.top,
177
+ w: canvas.width,
178
+ h: canvas.height
179
+ },
180
+ player: player && {
181
+ w: player.width,
182
+ h: player.height
183
+ },
184
+ // 16:9 推导值 vs 实测值,两者差得多说明容器不是满宽 16:9
185
+ 播放器高_推导: Math.round(getVerticalPlayerHeight()),
186
+ 竖屏全屏: isVerticalFull,
187
+ 期望_dock高: expect.dock高,
188
+ 期望_canvas高: expect.canvas高,
189
+ 背景带高: visibleH,
190
+ 背景带占比: player !== null && player !== void 0 && player.height ? +(visibleH / player.height).toFixed(3) : null,
191
+ // 竖屏全屏期望 ≈ 底部工具条高度(canvas 底边 = 工具条顶边);
192
+ // 普通竖屏期望 ≈ -HIT_PADDING_Y(沉到播放器底边以下)
193
+ canvas底距播底: canvas && player ? +(player.bottom - canvas.bottom).toFixed(1) : null,
194
+ // > 0 说明 canvas 顶部露在 dock 外被裁,那部分收不到 touch
195
+ canvas顶溢出: dock && canvas ? +(dock.top - canvas.top).toFixed(1) : null
196
+ }));
197
+ });
198
+ }, 300);
199
+ return () => clearTimeout(timer);
200
+ }, [remountKey]);
201
+
114
202
  // 点击播放器 → 收起转盘。
115
203
  // ui.tsx 的 handPlayerTap 会在此时把其余控件重新显示出来(componentHideState → false)
116
204
  // 并 emit(playerTap),所以这里只需负责关掉转盘。
@@ -156,7 +244,8 @@ export const ZoomTurntableDock = props => {
156
244
  lastVibrateValueRef.current = value;
157
245
  beginDrag(value);
158
246
 
159
- // 跨过主刻度(各镜头光学倍数 / 上限)时给更明显的反馈,形成挡位感
247
+ // 跨过主刻度(各镜头光学倍数 / 上限)时给更明显的反馈,形成挡位感。
248
+ // 这一下不受节流约束 —— 挡位感是转盘手感的骨架,丢一次就没了
160
249
  const crossedMark = marks.some(mark => prev < mark.value && value >= mark.value || prev > mark.value && value <= mark.value);
161
250
  const now = Date.now();
162
251
  if (crossedMark) {
@@ -164,10 +253,17 @@ export const ZoomTurntableDock = props => {
164
253
  vibrate('medium');
165
254
  return;
166
255
  }
167
- if (now - lastVibrateAtRef.current >= VIBRATE_THROTTLE) {
168
- lastVibrateAtRef.current = now;
169
- vibrate('light');
170
- }
256
+
257
+ // 距上一次震动太近就跳过。也顺带给 medium 留出静默期:
258
+ // 刚过主刻度又紧跟一下 light,会把挡位那一下的辨识度糊掉
259
+ if (now - lastVibrateAtRef.current < VIBRATE_THROTTLE) return;
260
+
261
+ // 一次 onChange 跨了好几格 = 正在快速甩动,逐格震分辨不出来,只会连成一片。
262
+ // 这时不震普通刻度,把反馈让给主刻度的挡位感
263
+ const movedSteps = step > 0 ? Math.abs(value - prev) / step : 1;
264
+ if (movedSteps > VIBRATE_FAST_STEPS) return;
265
+ lastVibrateAtRef.current = now;
266
+ vibrate('light');
171
267
  });
172
268
  const handleChangeEnd = useMemoizedFn(value => {
173
269
  applyZoom(value);
@@ -184,6 +280,18 @@ export const ZoomTurntableDock = props => {
184
280
  setNativeDisabled(false);
185
281
  });
186
282
 
283
+ /**
284
+ * 点在转盘空白处(未命中刻度带)= 点画面 → 收起转盘。
285
+ *
286
+ * canvas 覆盖了播放器下部很大一片,且 touch 事件被它 preventDefault + stopPropagation,
287
+ * 播放器收不到这一下 tap,「点画面收起」在转盘范围内整个失效 ——
288
+ * 表现就是第一次点没反应、第二次点(落到画面上部)才收起。
289
+ */
290
+ const handleBlankTap = useMemoizedFn(() => {
291
+ setNativeDisabled(false);
292
+ requestClose();
293
+ });
294
+
187
295
  // 定位由承载的 CoverView 负责(absolutePosition),这里只做布局容器。
188
296
  const turntableStyle = useMemo(() => ({
189
297
  width: isRight ? 'auto' : '100%',
@@ -193,6 +301,21 @@ export const ZoomTurntableDock = props => {
193
301
  alignItems: 'center',
194
302
  justifyContent: 'flex-end'
195
303
  }), [isRight]);
304
+
305
+ /**
306
+ * canvas 的承载层:只负责居中,不再用负 margin 把 canvas 往下推 ——
307
+ * 实测负 margin 在 CoverView 里不生效,探出去的那截会原地显示出来,
308
+ * 转盘整体被顶高一截、底边还留一条透明缝。
309
+ * 现在椭圆截面精确等于可见高度(CROP_RATIO_BOTTOM),整体下沉交给 dock 的
310
+ * dock 的 bottom(竖屏全屏为正、普通竖屏为负),canvas 尺寸与命中区保持原样,手势不受影响。
311
+ */
312
+ const canvasHolderStyle = useMemo(() => ({
313
+ display: 'flex',
314
+ justifyContent: 'center'
315
+ }), []);
316
+
317
+ /** 竖屏几何按容器 16:9 推导(转盘占播放器高的 1/3),横屏沿用设计稿比例 */
318
+ const styleOptions = useMemo(() => isRight ? STYLE_OPTIONS_RIGHT : getStyleOptionsBottom(isVerticalFull), [isRight, isVerticalFull]);
196
319
  if (!multiLensReady || playState !== PlayState.PLAYING) {
197
320
  return null;
198
321
  }
@@ -205,10 +328,16 @@ export const ZoomTurntableDock = props => {
205
328
  'ipc-player-plugin-zoom-turntable-dock-right': isRight
206
329
  }),
207
330
  style: turntableStyle
331
+ }, /*#__PURE__*/React.createElement(View, {
332
+ style: canvasHolderStyle
208
333
  }, /*#__PURE__*/React.createElement(ZoomTurntable, {
209
334
  key: `${isRight ? 'right' : 'bottom'}-${remountKey}`,
335
+ defaultValue: initialValue
336
+ // 受控回写:手势缩放画面时读数要跟着走。
337
+ // rjs 的 setValue 自带保护 —— 拖动中、以及松手后 300ms 冷却期内一律忽略回写,
338
+ // 所以不会出现「拖动被回写拽走」(那正是当初只传 defaultValue 的顾虑)。
339
+ ,
210
340
  value: curM,
211
- defaultValue: curM,
212
341
  min: min,
213
342
  max: max,
214
343
  step: step,
@@ -219,10 +348,11 @@ export const ZoomTurntableDock = props => {
219
348
  orientation: isRight ? 'right' : 'bottom',
220
349
  cropRatio: isRight ? CROP_RATIO_RIGHT : CROP_RATIO_BOTTOM,
221
350
  activeColor: "#FFFFFF",
222
- styleOptions: isRight ? STYLE_OPTIONS_RIGHT : STYLE_OPTIONS_BOTTOM,
351
+ styleOptions: styleOptions,
223
352
  onChange: handleChange,
224
353
  onChangeEnd: handleChangeEnd,
225
354
  onTouchStart: handleTouchStart,
226
- onTouchEnd: handleTouchEnd
227
- }));
355
+ onTouchEnd: handleTouchEnd,
356
+ onBlankTap: handleBlankTap
357
+ })));
228
358
  };