|
1 | | -import { |
2 | | - Animated, |
3 | | - PanResponder, |
4 | | - StyleSheet, |
5 | | - TextStyle, |
6 | | - View, |
7 | | - ViewStyle, |
8 | | - Platform, |
9 | | -} from 'react-native'; |
10 | | -import React, { memo, useMemo, useRef } from 'react'; |
11 | | -import { sin } from './AnimatedMath'; |
12 | | -import { CALENDAR_HEIGHT } from '../../enums'; |
| 1 | +import React from 'react'; |
| 2 | +import { Platform } from 'react-native'; |
| 3 | +import WheelNative from './WheelNative'; |
| 4 | +import WheelWeb from './WheelWeb'; |
13 | 5 |
|
14 | | -export interface WheelStyleProps { |
15 | | - containerStyle?: ViewStyle; |
16 | | - itemHeight?: number; |
17 | | - selectedColor?: string; |
18 | | - disabledColor?: string; |
19 | | - textStyle?: TextStyle; |
20 | | - wheelHeight?: number; |
21 | | - displayCount?: number; |
22 | | -} |
23 | | - |
24 | | -export interface WheelProps extends WheelStyleProps { |
| 6 | +interface WheelProps { |
25 | 7 | value: number; |
26 | 8 | setValue?: (value: number) => void; |
27 | | - items: number[]; |
| 9 | + items: string[]; |
28 | 10 | } |
29 | 11 |
|
30 | | -const Wheel = ({ |
31 | | - value, |
32 | | - setValue = () => {}, |
33 | | - items, |
34 | | - containerStyle, |
35 | | - textStyle, |
36 | | - itemHeight, |
37 | | - selectedColor = 'black', |
38 | | - disabledColor = 'gray', |
39 | | - wheelHeight, |
40 | | - displayCount = 5, |
41 | | -}: WheelProps) => { |
42 | | - const translateY = useRef(new Animated.Value(0)).current; |
43 | | - const renderCount = |
44 | | - displayCount * 2 < items.length ? displayCount * 8 : displayCount * 2 - 1; |
45 | | - const circular = items.length >= displayCount; |
46 | | - const height = |
47 | | - typeof containerStyle?.height === 'number' ? containerStyle.height : 130; |
48 | | - const radius = wheelHeight != null ? wheelHeight / 2 : height / 2; |
49 | | - |
50 | | - const valueIndex = useMemo(() => items.indexOf(value), [items, value]); |
51 | | - |
52 | | - const panResponder = useMemo(() => { |
53 | | - return PanResponder.create({ |
54 | | - onMoveShouldSetPanResponder: () => true, |
55 | | - onStartShouldSetPanResponderCapture: () => true, |
56 | | - onPanResponderGrant: () => { |
57 | | - translateY.setValue(0); |
58 | | - }, |
59 | | - onPanResponderMove: (evt, gestureState) => { |
60 | | - translateY.setValue(gestureState.dy); |
61 | | - evt.stopPropagation(); |
62 | | - }, |
63 | | - onPanResponderRelease: (_, gestureState) => { |
64 | | - translateY.extractOffset(); |
65 | | - let newValueIndex = |
66 | | - valueIndex - |
67 | | - Math.round(gestureState.dy / ((radius * 2) / displayCount)); |
68 | | - if (circular) { |
69 | | - newValueIndex = (newValueIndex + items.length) % items.length; |
70 | | - } else { |
71 | | - if (newValueIndex < 0) { |
72 | | - newValueIndex = 0; |
73 | | - } else if (newValueIndex >= items.length) { |
74 | | - newValueIndex = items.length - 1; |
75 | | - } |
76 | | - } |
77 | | - const newValue = items[newValueIndex] || 0; |
78 | | - if (newValue === value) { |
79 | | - translateY.setOffset(0); |
80 | | - translateY.setValue(0); |
81 | | - } else { |
82 | | - setValue(newValue); |
83 | | - } |
84 | | - }, |
85 | | - }); |
86 | | - }, [ |
87 | | - circular, |
88 | | - displayCount, |
89 | | - radius, |
90 | | - setValue, |
91 | | - value, |
92 | | - valueIndex, |
93 | | - items, |
94 | | - translateY, |
95 | | - ]); |
96 | | - |
97 | | - const displayValues = useMemo(() => { |
98 | | - const centerIndex = Math.floor(renderCount / 2); |
99 | | - |
100 | | - return Array.from({ length: renderCount }, (_, index) => { |
101 | | - let targetIndex = valueIndex + index - centerIndex; |
102 | | - if (targetIndex < 0 || targetIndex >= items.length) { |
103 | | - if (!circular) { |
104 | | - return 0; |
105 | | - } |
106 | | - targetIndex = (targetIndex + items.length) % items.length; |
107 | | - } |
108 | | - return items[targetIndex] || 0; |
109 | | - }); |
110 | | - }, [renderCount, valueIndex, items, circular]); |
111 | | - |
112 | | - const animatedAngles = useMemo(() => { |
113 | | - //translateY.setValue(0); |
114 | | - translateY.setOffset(0); |
115 | | - const currentIndex = displayValues.indexOf(value); |
116 | | - return displayValues && displayValues.length > 0 |
117 | | - ? displayValues.map((_, index) => |
118 | | - translateY |
119 | | - .interpolate({ |
120 | | - inputRange: [-radius, radius], |
121 | | - outputRange: [ |
122 | | - -radius + |
123 | | - ((radius * 2) / displayCount) * (index - currentIndex), |
124 | | - radius + ((radius * 2) / displayCount) * (index - currentIndex), |
125 | | - ], |
126 | | - extrapolate: 'extend', |
127 | | - }) |
128 | | - .interpolate({ |
129 | | - inputRange: [-radius, radius], |
130 | | - outputRange: [-Math.PI / 2, Math.PI / 2], |
131 | | - extrapolate: 'clamp', |
132 | | - }) |
133 | | - ) |
134 | | - : []; |
135 | | - }, [displayValues, radius, value, displayCount, translateY]); |
136 | | - |
137 | | - return ( |
138 | | - <View |
139 | | - style={[styles.container, containerStyle]} |
140 | | - {...panResponder.panHandlers} |
141 | | - > |
142 | | - {displayValues?.map((displayValue, index) => { |
143 | | - const animatedAngle = animatedAngles[index]; |
144 | | - return ( |
145 | | - <Animated.Text |
146 | | - key={`${displayValue}-${index}`} |
147 | | - style={[ |
148 | | - textStyle, |
149 | | - // eslint-disable-next-line react-native/no-inline-styles |
150 | | - { |
151 | | - position: 'absolute', |
152 | | - height: itemHeight, |
153 | | - transform: animatedAngle |
154 | | - ? [ |
155 | | - { |
156 | | - translateY: Animated.multiply( |
157 | | - radius, |
158 | | - sin(animatedAngle) |
159 | | - ), |
160 | | - }, |
161 | | - { |
162 | | - rotateX: animatedAngle.interpolate({ |
163 | | - inputRange: [-Math.PI / 2, Math.PI / 2], |
164 | | - outputRange: ['-89deg', '89deg'], |
165 | | - extrapolate: 'clamp', |
166 | | - }), |
167 | | - }, |
168 | | - ] |
169 | | - : [], |
170 | | - color: displayValue === value ? selectedColor : disabledColor, |
171 | | - }, |
172 | | - ]} |
173 | | - > |
174 | | - {typeof displayValue === 'number' && displayValue < 10 |
175 | | - ? `0${displayValue}` |
176 | | - : `${displayValue}`} |
177 | | - </Animated.Text> |
178 | | - ); |
179 | | - })} |
180 | | - </View> |
181 | | - ); |
182 | | -}; |
183 | | - |
184 | | -const styles = StyleSheet.create({ |
185 | | - container: { |
186 | | - minWidth: 30, |
187 | | - overflow: 'hidden', |
188 | | - alignItems: 'center', |
189 | | - justifyContent: 'center', |
190 | | - height: CALENDAR_HEIGHT / 2, |
191 | | - ...Platform.select({ |
192 | | - web: { |
193 | | - cursor: 'pointer', |
194 | | - userSelect: 'none', |
195 | | - }, |
196 | | - }), |
197 | | - }, |
198 | | - contentContainer: { |
199 | | - justifyContent: 'space-between', |
200 | | - alignItems: 'center', |
201 | | - }, |
202 | | -}); |
203 | | - |
204 | | -export default memo(Wheel, (prevProps, nextProps) => { |
205 | | - return ( |
206 | | - prevProps.value === nextProps.value && |
207 | | - prevProps.setValue === nextProps.setValue |
| 12 | +export default function Wheel(props: WheelProps) { |
| 13 | + return Platform.OS === 'web' ? ( |
| 14 | + <WheelWeb {...props} /> |
| 15 | + ) : ( |
| 16 | + <WheelNative {...props} /> |
208 | 17 | ); |
209 | | -}); |
| 18 | +} |
0 commit comments