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.
@@ -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/Web 走自绘 transform 路径。
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
- export const WHEEL_ITEM_HEIGHT =
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 = WHEEL_ITEM_HEIGHT * Math.floor(WHEEL_VISIBLE_ITEMS / 2);
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 ANDROID_VELOCITY_WINDOW_MS = 90;
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 * WHEEL_ITEM_HEIGHT;
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
- const currentIndexFloat = offset / WHEEL_ITEM_HEIGHT;
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
- const distanceItems = Math.abs(toOffset - fromOffset) / WHEEL_ITEM_HEIGHT;
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
- ? flattenedItemTextStyle.fontSize
324
- : IOS_NATIVE_PICKER_FONT_SIZE;
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
- () => ({ scrollToIndex, scrollToValue, settleToNearest, syncCurrentSelection }),
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 items = React.useMemo(
639
+ const webItems = React.useMemo(
654
640
  () =>
655
- options.map((item, index) => (
656
- <View key={`${String(optionKey(item, index))}-${index}`} style={styles.itemContainer}>
657
- <Animated.Text
658
- accessibilityLabel={item.accessibilityLabel ?? item.label}
659
- maxFontSizeMultiplier={getMaxFontSizeMultiplier()}
660
- numberOfLines={numberOfLines}
661
- style={[
662
- styles.itemText,
663
- { color: itemTextColor },
664
- itemTextStyle,
665
- index === displayIndex && [styles.itemTextSelected, { color: selectedTextColor }, selectedItemTextStyle],
666
- item.disabled && [styles.itemTextDisabled, { color: disabledTextColor }, disabledItemTextStyle],
667
- ]}
668
- testID={item.testID}
669
- >
670
- {item.label}
671
- </Animated.Text>
672
- </View>
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
- options.map((item, index) => ({
690
- label: item.label,
691
- value: item.value,
692
- textColor: item.disabled ? disabledTextColor : undefined,
693
- testID: item.testID ?? `wheel-item-${String(item.value)}-${index}`,
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
- return (
743
- <View
744
- {...viewProps}
745
- accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
746
- accessibilityHint={accessibilityHint}
747
- accessibilityLabel={accessibilityLabel}
748
- accessibilityRole="adjustable"
749
- accessibilityState={resolvedAccessibilityState}
750
- accessibilityValue={{ text: selectedOption?.label ?? '' }}
751
- collapsable={false}
752
- onAccessibilityAction={handleAccessibilityAction}
753
- style={columnStyle}
754
- testID={testID}
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
- {items}
763
- </Animated.View>
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
- style={styles.touchSurface}
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
- pointerEvents="box-only"
768
- onStartShouldSetResponder={() => canInteract}
769
- onStartShouldSetResponderCapture={() => canInteract}
770
- onMoveShouldSetResponder={() => canInteract}
771
- onMoveShouldSetResponderCapture={() => canInteract}
772
- onResponderTerminationRequest={() => false}
773
- onResponderGrant={(event) => {
774
- beginTouch(event.nativeEvent.pageY, event.nativeEvent.timestamp);
775
- }}
776
- onResponderMove={(event) => {
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