zkit-ui 2.0.9 → 2.0.11
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.
- package/dist/ui/Checkbox/index.d.ts.map +1 -1
- package/dist/ui/Checkbox/index.js +15 -15
- package/dist/ui/Switch/index.d.ts.map +1 -1
- package/dist/ui/Switch/index.js +14 -6
- package/dist/ui/WheelColumn/AndroidVirtualizedWheel.d.ts +26 -0
- package/dist/ui/WheelColumn/AndroidVirtualizedWheel.d.ts.map +1 -0
- package/dist/ui/WheelColumn/AndroidVirtualizedWheel.js +174 -0
- package/dist/ui/WheelColumn/index.d.ts.map +1 -1
- package/dist/ui/WheelColumn/index.js +162 -100
- package/package.json +1 -1
- package/src/ui/Checkbox/index.tsx +23 -15
- package/src/ui/Picker/README.md +1 -1
- package/src/ui/Switch/index.tsx +19 -7
- package/src/ui/WheelColumn/AndroidVirtualizedWheel.tsx +263 -0
- package/src/ui/WheelColumn/README.md +1 -1
- package/src/ui/WheelColumn/index.tsx +243 -169
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
4
|
StyleSheet,
|
|
5
|
+
Text as NativeText,
|
|
5
6
|
View,
|
|
6
7
|
type AccessibilityActionEvent,
|
|
7
8
|
type ColorValue,
|
|
@@ -17,9 +18,11 @@ import Animated, {
|
|
|
17
18
|
useSharedValue,
|
|
18
19
|
withTiming,
|
|
19
20
|
} from 'react-native-reanimated';
|
|
21
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
20
22
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
21
23
|
import { getMaxFontSizeMultiplier, sp, wp } from 'zkit-tools';
|
|
22
24
|
import { useTheme } from '../../theme/useTheme';
|
|
25
|
+
import { AndroidVirtualizedWheel, type AndroidVirtualizedWheelHandle } from './AndroidVirtualizedWheel';
|
|
23
26
|
import {
|
|
24
27
|
ZKitWheelPicker,
|
|
25
28
|
syncZKitWheelPickerCurrentSelection,
|
|
@@ -29,7 +32,7 @@ export { WHEEL_SELECTION_BACKGROUND_COLOR } from './constants';
|
|
|
29
32
|
|
|
30
33
|
export const WHEEL_VISIBLE_ITEMS = 5;
|
|
31
34
|
|
|
32
|
-
// iOS 走系统 UIPickerView,Android
|
|
35
|
+
// iOS 走系统 UIPickerView,Android 走原生虚拟列表,Web 走自绘 transform 路径。
|
|
33
36
|
// 这些尺寸是当前 app 端多轮调校后的稳定观感,保留为默认公共规格。
|
|
34
37
|
const IOS_CONFIRM_SYNC_TIMEOUT_MS = 64;
|
|
35
38
|
const IOS_NATIVE_PICKER_HEIGHT = wp(260);
|
|
@@ -42,10 +45,11 @@ export const WHEEL_VIEWPORT_HEIGHT = Platform.OS === 'ios' ? IOS_NATIVE_PICKER_H
|
|
|
42
45
|
export const WHEEL_AREA_HEIGHT =
|
|
43
46
|
Platform.OS === 'ios' ? Math.max(IOS_NATIVE_PICKER_HEIGHT, BASE_WHEEL_AREA_HEIGHT) : BASE_WHEEL_AREA_HEIGHT;
|
|
44
47
|
export const WHEEL_AREA_VERTICAL_INSET = Math.max(0, WHEEL_AREA_HEIGHT - WHEEL_VIEWPORT_HEIGHT) / 2;
|
|
45
|
-
|
|
48
|
+
const INTERNAL_WHEEL_ITEM_HEIGHT =
|
|
46
49
|
Platform.OS === 'ios' ? IOS_NATIVE_PICKER_HEIGHT / WHEEL_VISIBLE_ITEMS : BASE_WHEEL_ITEM_HEIGHT;
|
|
50
|
+
export const WHEEL_ITEM_HEIGHT = INTERNAL_WHEEL_ITEM_HEIGHT;
|
|
47
51
|
|
|
48
|
-
const CENTER_OFFSET =
|
|
52
|
+
const CENTER_OFFSET = INTERNAL_WHEEL_ITEM_HEIGHT * Math.floor(WHEEL_VISIBLE_ITEMS / 2);
|
|
49
53
|
const IOS_SETTLE_DELAY_MS = 90;
|
|
50
54
|
|
|
51
55
|
const SNAP_DURATION_MIN = 140;
|
|
@@ -56,9 +60,14 @@ const RELEASE_VELOCITY_DEADZONE = 220;
|
|
|
56
60
|
const RELEASE_LOCK_DISTANCE_ITEMS = 0.1;
|
|
57
61
|
const RELEASE_LOCK_MAX_VELOCITY = 380;
|
|
58
62
|
const MAX_FLING_ITEMS = 7;
|
|
59
|
-
const
|
|
63
|
+
const PAN_ACTIVATION_OFFSET = wp(3);
|
|
60
64
|
|
|
61
65
|
const SNAP_EASING = Easing.bezier(0.22, 1, 0.36, 1);
|
|
66
|
+
// tsc 输出 CommonJS 后,worklet 若直接引用导入命名空间会把整个原生模块放进闭包。
|
|
67
|
+
// 先保存具体函数引用,消费端 worklet 插件即可只序列化函数本身。
|
|
68
|
+
const cancelAnimationOnUI = cancelAnimation;
|
|
69
|
+
const withTimingOnUI = withTiming;
|
|
70
|
+
const scheduleOnRNFromUI = scheduleOnRN;
|
|
62
71
|
|
|
63
72
|
export type WheelColumnValue = string | number;
|
|
64
73
|
|
|
@@ -115,10 +124,11 @@ function clampNumber(n: number, min: number, max: number) {
|
|
|
115
124
|
|
|
116
125
|
function indexToOffset(index: number) {
|
|
117
126
|
'worklet';
|
|
118
|
-
return index *
|
|
127
|
+
return index * INTERNAL_WHEEL_ITEM_HEIGHT;
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
function getReleaseDeltaItems(velocityY: number) {
|
|
131
|
+
'worklet';
|
|
122
132
|
const speed = Math.abs(velocityY);
|
|
123
133
|
if (speed < RELEASE_VELOCITY_DEADZONE) return 0;
|
|
124
134
|
const projected = Math.pow(speed / 1400, 1.05) * 3.6;
|
|
@@ -126,7 +136,8 @@ function getReleaseDeltaItems(velocityY: number) {
|
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
function getTargetIndexFromRelease(offset: number, velocityY: number, maxIndex: number) {
|
|
129
|
-
|
|
139
|
+
'worklet';
|
|
140
|
+
const currentIndexFloat = offset / INTERNAL_WHEEL_ITEM_HEIGHT;
|
|
130
141
|
const nearestIndex = Math.round(currentIndexFloat);
|
|
131
142
|
const distanceToNearest = Math.abs(currentIndexFloat - nearestIndex);
|
|
132
143
|
const speed = Math.abs(velocityY);
|
|
@@ -155,7 +166,8 @@ function getTargetIndexFromRelease(offset: number, velocityY: number, maxIndex:
|
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
function getSnapDuration(fromOffset: number, toOffset: number) {
|
|
158
|
-
|
|
169
|
+
'worklet';
|
|
170
|
+
const distanceItems = Math.abs(toOffset - fromOffset) / INTERNAL_WHEEL_ITEM_HEIGHT;
|
|
159
171
|
return clampNumber(
|
|
160
172
|
Math.round(SNAP_DURATION_MIN + distanceItems * SNAP_DURATION_PER_ITEM),
|
|
161
173
|
SNAP_DURATION_MIN,
|
|
@@ -216,6 +228,50 @@ function findNearestEnabledIndex<TValue extends WheelColumnValue>(
|
|
|
216
228
|
return -1;
|
|
217
229
|
}
|
|
218
230
|
|
|
231
|
+
function findNearestEnabledFlagIndex(enabledFlags: readonly boolean[], index: number, direction = 0) {
|
|
232
|
+
'worklet';
|
|
233
|
+
if (!enabledFlags.length) return -1;
|
|
234
|
+
|
|
235
|
+
const clampedIndex = clampNumber(Math.round(index), 0, enabledFlags.length - 1);
|
|
236
|
+
if (enabledFlags[clampedIndex]) return clampedIndex;
|
|
237
|
+
|
|
238
|
+
const walkForward = () => {
|
|
239
|
+
'worklet';
|
|
240
|
+
for (let i = clampedIndex + 1; i < enabledFlags.length; i += 1) {
|
|
241
|
+
if (enabledFlags[i]) return i;
|
|
242
|
+
}
|
|
243
|
+
return -1;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const walkBackward = () => {
|
|
247
|
+
'worklet';
|
|
248
|
+
for (let i = clampedIndex - 1; i >= 0; i -= 1) {
|
|
249
|
+
if (enabledFlags[i]) return i;
|
|
250
|
+
}
|
|
251
|
+
return -1;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (direction > 0) {
|
|
255
|
+
const next = walkForward();
|
|
256
|
+
return next >= 0 ? next : walkBackward();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (direction < 0) {
|
|
260
|
+
const previous = walkBackward();
|
|
261
|
+
return previous >= 0 ? previous : walkForward();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (let distance = 1; distance < enabledFlags.length; distance += 1) {
|
|
265
|
+
const previous = clampedIndex - distance;
|
|
266
|
+
if (previous >= 0 && enabledFlags[previous]) return previous;
|
|
267
|
+
|
|
268
|
+
const next = clampedIndex + distance;
|
|
269
|
+
if (next < enabledFlags.length && enabledFlags[next]) return next;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return -1;
|
|
273
|
+
}
|
|
274
|
+
|
|
219
275
|
function resolveDisplayIndex<TValue extends WheelColumnValue>(
|
|
220
276
|
options: readonly WheelColumnOption<TValue>[],
|
|
221
277
|
value: TValue | null | undefined
|
|
@@ -292,6 +348,7 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
292
348
|
const isUserInteracting = useSharedValue(false);
|
|
293
349
|
|
|
294
350
|
const iosPickerRef = React.useRef<unknown>(null);
|
|
351
|
+
const androidWheelRef = React.useRef<AndroidVirtualizedWheelHandle>(null);
|
|
295
352
|
const iosSelectedIndexRef = React.useRef(displayIndex);
|
|
296
353
|
const iosSettleTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
297
354
|
const iosSyncTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -320,8 +377,8 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
320
377
|
typeof flattenedSelectedItemTextStyle.fontSize === 'number'
|
|
321
378
|
? flattenedSelectedItemTextStyle.fontSize
|
|
322
379
|
: typeof flattenedItemTextStyle.fontSize === 'number'
|
|
323
|
-
|
|
324
|
-
|
|
380
|
+
? flattenedItemTextStyle.fontSize
|
|
381
|
+
: IOS_NATIVE_PICKER_FONT_SIZE;
|
|
325
382
|
const nativeFontWeight =
|
|
326
383
|
normalizeFontWeight(flattenedSelectedItemTextStyle.fontWeight) ??
|
|
327
384
|
normalizeFontWeight(flattenedItemTextStyle.fontWeight) ??
|
|
@@ -382,8 +439,7 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
382
439
|
const option = options[nextIndex];
|
|
383
440
|
if (!option || option.disabled) return;
|
|
384
441
|
|
|
385
|
-
const sameAsCurrent =
|
|
386
|
-
displayIndexRef.current === nextIndex && Object.is(selectedValueRef.current, option.value);
|
|
442
|
+
const sameAsCurrent = displayIndexRef.current === nextIndex && Object.is(selectedValueRef.current, option.value);
|
|
387
443
|
|
|
388
444
|
if (sameAsCurrent) return;
|
|
389
445
|
|
|
@@ -428,6 +484,11 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
428
484
|
return;
|
|
429
485
|
}
|
|
430
486
|
|
|
487
|
+
if (Platform.OS === 'android') {
|
|
488
|
+
androidWheelRef.current?.scrollToIndex(nextIndex, animated);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
431
492
|
const nextOffset = indexToOffset(nextIndex);
|
|
432
493
|
cancelAnimation(offsetY);
|
|
433
494
|
isUserInteracting.value = false;
|
|
@@ -454,6 +515,10 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
454
515
|
|
|
455
516
|
const settleToNearest = React.useCallback(
|
|
456
517
|
(animated = false) => {
|
|
518
|
+
if (Platform.OS === 'android') {
|
|
519
|
+
return androidWheelRef.current?.settleToNearest(animated) ?? resolveSelectableIndex(displayIndexRef.current);
|
|
520
|
+
}
|
|
521
|
+
|
|
457
522
|
const rawIndex =
|
|
458
523
|
Platform.OS === 'ios'
|
|
459
524
|
? iosSelectedIndexRef.current
|
|
@@ -489,7 +554,12 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
489
554
|
|
|
490
555
|
React.useImperativeHandle(
|
|
491
556
|
ref,
|
|
492
|
-
() => ({
|
|
557
|
+
() => ({
|
|
558
|
+
scrollToIndex,
|
|
559
|
+
scrollToValue,
|
|
560
|
+
settleToNearest,
|
|
561
|
+
syncCurrentSelection,
|
|
562
|
+
}),
|
|
493
563
|
[scrollToIndex, scrollToValue, settleToNearest, syncCurrentSelection]
|
|
494
564
|
);
|
|
495
565
|
|
|
@@ -540,95 +610,6 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
540
610
|
[resolveIOSSyncRequest, resolveSelectableIndex, scheduleIOSSettle, syncIOSSelectedIndex]
|
|
541
611
|
);
|
|
542
612
|
|
|
543
|
-
const startInteraction = React.useCallback(() => {
|
|
544
|
-
cancelAnimation(offsetY);
|
|
545
|
-
isUserInteracting.value = true;
|
|
546
|
-
dragStartOffset.value = offsetY.value;
|
|
547
|
-
}, [dragStartOffset, isUserInteracting, offsetY]);
|
|
548
|
-
|
|
549
|
-
const updateInteraction = React.useCallback(
|
|
550
|
-
(translationY: number) => {
|
|
551
|
-
offsetY.value = clampNumber(dragStartOffset.value - translationY, 0, maxOffset);
|
|
552
|
-
},
|
|
553
|
-
[dragStartOffset, maxOffset, offsetY]
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
const finishInteraction = React.useCallback(
|
|
557
|
-
(velocityY: number) => {
|
|
558
|
-
const currentOffset = clampNumber(offsetY.value, 0, maxOffset);
|
|
559
|
-
const rawIndex = getTargetIndexFromRelease(currentOffset, velocityY, maxIndex);
|
|
560
|
-
const currentIndex = Math.round(currentOffset / WHEEL_ITEM_HEIGHT);
|
|
561
|
-
const nextIndex = resolveSelectableIndex(rawIndex, rawIndex - currentIndex);
|
|
562
|
-
const nextOffset = indexToOffset(nextIndex);
|
|
563
|
-
offsetY.value = withTiming(
|
|
564
|
-
nextOffset,
|
|
565
|
-
{
|
|
566
|
-
duration: getSnapDuration(currentOffset, nextOffset),
|
|
567
|
-
easing: SNAP_EASING,
|
|
568
|
-
},
|
|
569
|
-
(finished) => {
|
|
570
|
-
if (!finished) return;
|
|
571
|
-
isUserInteracting.value = false;
|
|
572
|
-
scheduleOnRN(emitSelectedIndex, nextIndex, 'user');
|
|
573
|
-
}
|
|
574
|
-
);
|
|
575
|
-
},
|
|
576
|
-
[emitSelectedIndex, isUserInteracting, maxIndex, maxOffset, offsetY, resolveSelectableIndex]
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
const touchStateRef = React.useRef({
|
|
580
|
-
startPageY: 0,
|
|
581
|
-
samples: [] as Array<{ pageY: number; timestamp: number }>,
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
const beginTouch = React.useCallback(
|
|
585
|
-
(pageY: number, timestamp?: number) => {
|
|
586
|
-
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
|
587
|
-
touchStateRef.current = {
|
|
588
|
-
startPageY: pageY,
|
|
589
|
-
samples: [{ pageY, timestamp: ts }],
|
|
590
|
-
};
|
|
591
|
-
startInteraction();
|
|
592
|
-
},
|
|
593
|
-
[startInteraction]
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
const recordTouchSample = React.useCallback((pageY: number, timestamp?: number) => {
|
|
597
|
-
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
|
598
|
-
const { samples } = touchStateRef.current;
|
|
599
|
-
samples.push({ pageY, timestamp: ts });
|
|
600
|
-
|
|
601
|
-
while (samples.length > 6) {
|
|
602
|
-
samples.shift();
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const minTs = ts - ANDROID_VELOCITY_WINDOW_MS;
|
|
606
|
-
while (samples.length > 2 && samples[0]?.timestamp < minTs) {
|
|
607
|
-
samples.shift();
|
|
608
|
-
}
|
|
609
|
-
}, []);
|
|
610
|
-
|
|
611
|
-
const moveTouch = React.useCallback(
|
|
612
|
-
(pageY: number, timestamp?: number) => {
|
|
613
|
-
updateInteraction(pageY - touchStateRef.current.startPageY);
|
|
614
|
-
recordTouchSample(pageY, timestamp);
|
|
615
|
-
},
|
|
616
|
-
[recordTouchSample, updateInteraction]
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
const endTouch = React.useCallback(
|
|
620
|
-
(pageY: number, timestamp?: number) => {
|
|
621
|
-
recordTouchSample(pageY, timestamp);
|
|
622
|
-
const { samples } = touchStateRef.current;
|
|
623
|
-
const firstSample = samples[0];
|
|
624
|
-
const lastSample = samples[samples.length - 1];
|
|
625
|
-
const deltaY = (lastSample?.pageY ?? pageY) - (firstSample?.pageY ?? pageY);
|
|
626
|
-
const deltaT = Math.max(1, (lastSample?.timestamp ?? Date.now()) - (firstSample?.timestamp ?? Date.now()));
|
|
627
|
-
finishInteraction((deltaY / deltaT) * 1000);
|
|
628
|
-
},
|
|
629
|
-
[finishInteraction, recordTouchSample]
|
|
630
|
-
);
|
|
631
|
-
|
|
632
613
|
const handleAccessibilityAction = React.useCallback(
|
|
633
614
|
(event: AccessibilityActionEvent) => {
|
|
634
615
|
if (disabled) return;
|
|
@@ -646,31 +627,42 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
646
627
|
[disabled, emitSelectedIndex, options, scrollToIndex]
|
|
647
628
|
);
|
|
648
629
|
|
|
630
|
+
const handleAndroidSelectIndex = React.useCallback(
|
|
631
|
+
(nextIndex: number) => emitSelectedIndex(nextIndex, 'user'),
|
|
632
|
+
[emitSelectedIndex]
|
|
633
|
+
);
|
|
634
|
+
|
|
649
635
|
const contentStyle = useAnimatedStyle(() => ({
|
|
650
636
|
transform: [{ translateY: CENTER_OFFSET - offsetY.value }],
|
|
651
637
|
}));
|
|
652
638
|
|
|
653
|
-
const
|
|
639
|
+
const webItems = React.useMemo(
|
|
654
640
|
() =>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
641
|
+
Platform.OS === 'web'
|
|
642
|
+
? options.map((item, index) => (
|
|
643
|
+
<View key={`${String(optionKey(item, index))}-${index}`} style={styles.itemContainer}>
|
|
644
|
+
<NativeText
|
|
645
|
+
accessibilityLabel={item.accessibilityLabel ?? item.label}
|
|
646
|
+
maxFontSizeMultiplier={getMaxFontSizeMultiplier()}
|
|
647
|
+
numberOfLines={numberOfLines}
|
|
648
|
+
style={[
|
|
649
|
+
styles.itemText,
|
|
650
|
+
{ color: itemTextColor },
|
|
651
|
+
itemTextStyle,
|
|
652
|
+
index === displayIndex && [
|
|
653
|
+
styles.itemTextSelected,
|
|
654
|
+
{ color: selectedTextColor },
|
|
655
|
+
selectedItemTextStyle,
|
|
656
|
+
],
|
|
657
|
+
item.disabled && [styles.itemTextDisabled, { color: disabledTextColor }, disabledItemTextStyle],
|
|
658
|
+
]}
|
|
659
|
+
testID={item.testID}
|
|
660
|
+
>
|
|
661
|
+
{item.label}
|
|
662
|
+
</NativeText>
|
|
663
|
+
</View>
|
|
664
|
+
))
|
|
665
|
+
: null,
|
|
674
666
|
[
|
|
675
667
|
disabledItemTextStyle,
|
|
676
668
|
disabledTextColor,
|
|
@@ -686,17 +678,88 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
686
678
|
|
|
687
679
|
const nativeItems = React.useMemo(
|
|
688
680
|
() =>
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
681
|
+
Platform.OS === 'ios'
|
|
682
|
+
? options.map((item, index) => ({
|
|
683
|
+
label: item.label,
|
|
684
|
+
value: item.value,
|
|
685
|
+
textColor: item.disabled ? disabledTextColor : undefined,
|
|
686
|
+
testID: item.testID ?? `wheel-item-${String(item.value)}-${index}`,
|
|
687
|
+
}))
|
|
688
|
+
: [],
|
|
695
689
|
[disabledTextColor, options]
|
|
696
690
|
);
|
|
697
691
|
|
|
698
692
|
const selectedOption = options[displayIndex];
|
|
699
693
|
const canInteract = !disabled && options.length > 1 && findNearestEnabledIndex(options, displayIndex) >= 0;
|
|
694
|
+
const enabledFlags = React.useMemo(
|
|
695
|
+
() => (Platform.OS === 'web' ? options.map((option) => !option.disabled) : []),
|
|
696
|
+
[options]
|
|
697
|
+
);
|
|
698
|
+
const panGesture = React.useMemo(
|
|
699
|
+
() =>
|
|
700
|
+
Gesture.Pan()
|
|
701
|
+
.enabled(Platform.OS === 'web' && canInteract)
|
|
702
|
+
.activeOffsetY([-PAN_ACTIVATION_OFFSET, PAN_ACTIVATION_OFFSET])
|
|
703
|
+
.averageTouches(true)
|
|
704
|
+
.onStart(() => {
|
|
705
|
+
'worklet';
|
|
706
|
+
cancelAnimationOnUI(offsetY);
|
|
707
|
+
isUserInteracting.value = true;
|
|
708
|
+
dragStartOffset.value = offsetY.value;
|
|
709
|
+
})
|
|
710
|
+
.onUpdate((event) => {
|
|
711
|
+
'worklet';
|
|
712
|
+
offsetY.value = clampNumber(dragStartOffset.value - event.translationY, 0, maxOffset);
|
|
713
|
+
})
|
|
714
|
+
.onEnd((event) => {
|
|
715
|
+
'worklet';
|
|
716
|
+
const currentOffset = clampNumber(offsetY.value, 0, maxOffset);
|
|
717
|
+
const currentIndex = Math.round(currentOffset / INTERNAL_WHEEL_ITEM_HEIGHT);
|
|
718
|
+
const rawIndex = getTargetIndexFromRelease(currentOffset, event.velocityY, maxIndex);
|
|
719
|
+
const direction = rawIndex - currentIndex;
|
|
720
|
+
const enabledIndex = findNearestEnabledFlagIndex(enabledFlags, rawIndex, direction);
|
|
721
|
+
const nextIndex = enabledIndex >= 0 ? enabledIndex : clampNumber(rawIndex, 0, maxIndex);
|
|
722
|
+
const nextOffset = indexToOffset(nextIndex);
|
|
723
|
+
|
|
724
|
+
offsetY.value = withTimingOnUI(
|
|
725
|
+
nextOffset,
|
|
726
|
+
{
|
|
727
|
+
duration: getSnapDuration(currentOffset, nextOffset),
|
|
728
|
+
easing: SNAP_EASING,
|
|
729
|
+
},
|
|
730
|
+
(finished) => {
|
|
731
|
+
if (!finished) return;
|
|
732
|
+
isUserInteracting.value = false;
|
|
733
|
+
scheduleOnRNFromUI(emitSelectedIndex, nextIndex, 'user');
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
})
|
|
737
|
+
.onFinalize((_event, success) => {
|
|
738
|
+
'worklet';
|
|
739
|
+
if (success || !isUserInteracting.value) return;
|
|
740
|
+
|
|
741
|
+
const currentOffset = clampNumber(offsetY.value, 0, maxOffset);
|
|
742
|
+
const currentIndex = Math.round(currentOffset / INTERNAL_WHEEL_ITEM_HEIGHT);
|
|
743
|
+
const direction = currentOffset - dragStartOffset.value;
|
|
744
|
+
const enabledIndex = findNearestEnabledFlagIndex(enabledFlags, currentIndex, direction);
|
|
745
|
+
const nextIndex = enabledIndex >= 0 ? enabledIndex : clampNumber(currentIndex, 0, maxIndex);
|
|
746
|
+
const nextOffset = indexToOffset(nextIndex);
|
|
747
|
+
|
|
748
|
+
offsetY.value = withTimingOnUI(
|
|
749
|
+
nextOffset,
|
|
750
|
+
{
|
|
751
|
+
duration: getSnapDuration(currentOffset, nextOffset),
|
|
752
|
+
easing: SNAP_EASING,
|
|
753
|
+
},
|
|
754
|
+
(finished) => {
|
|
755
|
+
if (!finished) return;
|
|
756
|
+
isUserInteracting.value = false;
|
|
757
|
+
scheduleOnRNFromUI(emitSelectedIndex, nextIndex, 'user');
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
}),
|
|
761
|
+
[canInteract, dragStartOffset, emitSelectedIndex, enabledFlags, isUserInteracting, maxIndex, maxOffset, offsetY]
|
|
762
|
+
);
|
|
700
763
|
const columnStyle = React.useMemo(
|
|
701
764
|
() => [styles.column, width != null && { width }, resolveWebInteractionStyle(disabled), style],
|
|
702
765
|
[disabled, style, width]
|
|
@@ -739,51 +802,62 @@ const WheelColumnBase = React.forwardRef<WheelColumnHandle, WheelColumnProps>(fu
|
|
|
739
802
|
);
|
|
740
803
|
}
|
|
741
804
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
>
|
|
756
|
-
<Animated.View
|
|
757
|
-
style={[styles.content, contentStyle]}
|
|
758
|
-
pointerEvents="none"
|
|
759
|
-
renderToHardwareTextureAndroid={Platform.OS === 'android'}
|
|
760
|
-
shouldRasterizeIOS={false}
|
|
805
|
+
if (Platform.OS === 'android') {
|
|
806
|
+
return (
|
|
807
|
+
<View
|
|
808
|
+
{...viewProps}
|
|
809
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
810
|
+
accessibilityHint={accessibilityHint}
|
|
811
|
+
accessibilityLabel={accessibilityLabel}
|
|
812
|
+
accessibilityRole="adjustable"
|
|
813
|
+
accessibilityState={resolvedAccessibilityState}
|
|
814
|
+
accessibilityValue={{ text: selectedOption?.label ?? '' }}
|
|
815
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
816
|
+
style={columnStyle}
|
|
817
|
+
testID={testID}
|
|
761
818
|
>
|
|
762
|
-
|
|
763
|
-
|
|
819
|
+
<AndroidVirtualizedWheel
|
|
820
|
+
ref={androidWheelRef}
|
|
821
|
+
options={options}
|
|
822
|
+
selectedIndex={displayIndex}
|
|
823
|
+
canInteract={canInteract}
|
|
824
|
+
itemHeight={WHEEL_ITEM_HEIGHT}
|
|
825
|
+
visibleItems={WHEEL_VISIBLE_ITEMS}
|
|
826
|
+
itemTextColor={itemTextColor}
|
|
827
|
+
selectedTextColor={selectedTextColor}
|
|
828
|
+
disabledTextColor={disabledTextColor}
|
|
829
|
+
itemTextStyle={itemTextStyle}
|
|
830
|
+
selectedItemTextStyle={selectedItemTextStyle}
|
|
831
|
+
disabledItemTextStyle={disabledItemTextStyle}
|
|
832
|
+
numberOfLines={numberOfLines}
|
|
833
|
+
resolveSelectableIndex={resolveSelectableIndex}
|
|
834
|
+
onSelectIndex={handleAndroidSelectIndex}
|
|
835
|
+
/>
|
|
836
|
+
</View>
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return (
|
|
841
|
+
<GestureDetector gesture={panGesture} touchAction="none" userSelect="none">
|
|
764
842
|
<View
|
|
765
|
-
|
|
843
|
+
{...viewProps}
|
|
844
|
+
accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
|
|
845
|
+
accessibilityHint={accessibilityHint}
|
|
846
|
+
accessibilityLabel={accessibilityLabel}
|
|
847
|
+
accessibilityRole="adjustable"
|
|
848
|
+
accessibilityState={resolvedAccessibilityState}
|
|
849
|
+
accessibilityValue={{ text: selectedOption?.label ?? '' }}
|
|
766
850
|
collapsable={false}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
moveTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
|
|
778
|
-
}}
|
|
779
|
-
onResponderRelease={(event) => {
|
|
780
|
-
endTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
|
|
781
|
-
}}
|
|
782
|
-
onResponderTerminate={(event) => {
|
|
783
|
-
endTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
|
|
784
|
-
}}
|
|
785
|
-
/>
|
|
786
|
-
</View>
|
|
851
|
+
onAccessibilityAction={handleAccessibilityAction}
|
|
852
|
+
style={columnStyle}
|
|
853
|
+
testID={testID}
|
|
854
|
+
>
|
|
855
|
+
<Animated.View style={[styles.content, contentStyle]} pointerEvents="none">
|
|
856
|
+
{webItems}
|
|
857
|
+
</Animated.View>
|
|
858
|
+
<View style={styles.touchSurface} collapsable={false} pointerEvents="box-only" />
|
|
859
|
+
</View>
|
|
860
|
+
</GestureDetector>
|
|
787
861
|
);
|
|
788
862
|
});
|
|
789
863
|
|