expo-horizontal-picker 0.2.1 → 0.3.0
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/README.md +99 -40
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +42 -12
- package/build/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +83 -43
package/README.md
CHANGED
|
@@ -39,65 +39,117 @@ Make sure to follow the additional setup instructions for Reanimated in the [off
|
|
|
39
39
|

|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
import { HorizontalPicker, type HorizontalPickerRef
|
|
42
|
+
import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
|
|
43
43
|
import { useRef, useState } from 'react';
|
|
44
|
-
import { StyleSheet, Text
|
|
44
|
+
import { ScrollView, StyleSheet, Text } from 'react-native';
|
|
45
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
45
46
|
|
|
46
47
|
const numberItems = Array.from({ length: 600 }, (_, i) => ({
|
|
47
48
|
label: `${i + 1}`,
|
|
48
49
|
value: i + 1,
|
|
49
50
|
}));
|
|
50
51
|
|
|
52
|
+
const thousandItems = Array.from({ length: 20 }, (_, i) => ({
|
|
53
|
+
label: `${i + 1}k`,
|
|
54
|
+
value: (i + 1) * 1000,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const hourItems = Array.from({ length: 24 }, (_, i) => ({
|
|
58
|
+
label: `${i + 1}h`,
|
|
59
|
+
value: i + 1,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const largeNumberItems = Array.from({ length: 5 }, (_, i) => ({
|
|
63
|
+
label: `${(i + 1) * 10000}`,
|
|
64
|
+
value: (i + 1) * 10000,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const pickerContainerHeight = 53;
|
|
68
|
+
|
|
51
69
|
export default function App() {
|
|
52
70
|
const firstPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
53
71
|
const secondPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
54
|
-
const [selected, setSelected] = useState<PickerValues>({
|
|
55
|
-
index: 499,
|
|
56
|
-
value: numberItems[499].value,
|
|
57
|
-
});
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const [, setSelectedFirst] = useState({ index: 499, value: numberItems[499].value });
|
|
74
|
+
const [, setSelectedSecond] = useState({ index: 499, value: numberItems[499].value });
|
|
75
|
+
const [, setSelectedThird] = useState({ index: 9, value: thousandItems[9].value });
|
|
76
|
+
const [, setSelectedFourth] = useState({ index: 11, value: hourItems[11].value });
|
|
77
|
+
const [, setSelectedFifth] = useState({ index: 2, value: largeNumberItems[2].value });
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
return (
|
|
80
|
+
<SafeAreaView style={styles.container}>
|
|
81
|
+
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
82
|
+
<Text style={styles.title}>expo-horizontal-picker</Text>
|
|
83
|
+
|
|
84
|
+
{/* 7 visible items (sync) */}
|
|
85
|
+
<HorizontalPicker
|
|
86
|
+
ref={firstPickerRef}
|
|
87
|
+
items={numberItems}
|
|
88
|
+
containerHeight={pickerContainerHeight}
|
|
89
|
+
initialScrollIndex={499}
|
|
90
|
+
visibleItemCount={7}
|
|
91
|
+
onChange={(value, index) => {
|
|
92
|
+
setSelectedFirst({ index, value });
|
|
93
|
+
secondPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
94
|
+
}}
|
|
95
|
+
pickerItemStyle={styles.pickerItem}
|
|
96
|
+
/>
|
|
97
|
+
<HorizontalPicker
|
|
98
|
+
ref={secondPickerRef}
|
|
99
|
+
items={numberItems}
|
|
100
|
+
containerHeight={pickerContainerHeight}
|
|
101
|
+
initialScrollIndex={499}
|
|
102
|
+
visibleItemCount={7}
|
|
103
|
+
onChange={(value, index) => {
|
|
104
|
+
setSelectedSecond({ index, value });
|
|
105
|
+
firstPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
106
|
+
}}
|
|
107
|
+
pickerItemStyle={styles.pickerItem}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
{/* 5 / 3 / 1 visible items */}
|
|
111
|
+
<HorizontalPicker
|
|
112
|
+
items={thousandItems}
|
|
113
|
+
containerHeight={pickerContainerHeight}
|
|
114
|
+
initialScrollIndex={9}
|
|
115
|
+
visibleItemCount={5}
|
|
116
|
+
onChange={(value, index) => setSelectedThird({ index, value })}
|
|
117
|
+
pickerItemStyle={styles.pickerItem}
|
|
118
|
+
/>
|
|
119
|
+
<HorizontalPicker
|
|
120
|
+
items={hourItems}
|
|
121
|
+
containerHeight={pickerContainerHeight}
|
|
122
|
+
initialScrollIndex={11}
|
|
123
|
+
visibleItemCount={3}
|
|
124
|
+
onChange={(value, index) => setSelectedFourth({ index, value })}
|
|
125
|
+
pickerItemStyle={styles.pickerItem}
|
|
126
|
+
/>
|
|
127
|
+
<HorizontalPicker
|
|
128
|
+
items={largeNumberItems}
|
|
129
|
+
containerHeight={pickerContainerHeight}
|
|
130
|
+
initialScrollIndex={2}
|
|
131
|
+
visibleItemCount={1}
|
|
132
|
+
onChange={(value, index) => setSelectedFifth({ index, value })}
|
|
133
|
+
pickerItemStyle={styles.pickerItem}
|
|
134
|
+
/>
|
|
135
|
+
</ScrollView>
|
|
136
|
+
</SafeAreaView>
|
|
87
137
|
);
|
|
88
138
|
}
|
|
89
139
|
|
|
90
140
|
const styles = StyleSheet.create({
|
|
91
141
|
container: {
|
|
92
142
|
flex: 1,
|
|
93
|
-
justifyContent: 'center',
|
|
94
|
-
paddingHorizontal: 20,
|
|
95
143
|
backgroundColor: '#eeeeee',
|
|
96
144
|
},
|
|
145
|
+
content: {
|
|
146
|
+
paddingHorizontal: 20,
|
|
147
|
+
paddingVertical: 12,
|
|
148
|
+
gap: 16,
|
|
149
|
+
},
|
|
97
150
|
title: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
fontWeight: '700',
|
|
151
|
+
fontSize: 28,
|
|
152
|
+
fontWeight: '800',
|
|
101
153
|
color: '#111111',
|
|
102
154
|
},
|
|
103
155
|
pickerItem: {
|
|
@@ -132,6 +184,7 @@ export default function RefExample() {
|
|
|
132
184
|
<HorizontalPicker
|
|
133
185
|
ref={firstPickerRef}
|
|
134
186
|
items={items}
|
|
187
|
+
containerHeight={53}
|
|
135
188
|
onChange={(value, index) => {
|
|
136
189
|
setSelected({ index, value });
|
|
137
190
|
secondPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
@@ -141,6 +194,7 @@ export default function RefExample() {
|
|
|
141
194
|
<HorizontalPicker
|
|
142
195
|
ref={secondPickerRef}
|
|
143
196
|
items={items}
|
|
197
|
+
containerHeight={53}
|
|
144
198
|
onChange={(value, index) => {
|
|
145
199
|
setSelected({ index, value });
|
|
146
200
|
firstPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
@@ -153,7 +207,10 @@ export default function RefExample() {
|
|
|
153
207
|
|
|
154
208
|
## 📱 Example App
|
|
155
209
|
|
|
156
|
-
A runnable Expo example app is included in [`example`](./example). It
|
|
210
|
+
A runnable Expo example app is included in [`example`](./example). It includes:
|
|
211
|
+
|
|
212
|
+
- two synced pickers (`visibleItemCount={7}`) driven by refs
|
|
213
|
+
- additional standalone pickers with `visibleItemCount` set to `5`, `3`, and `1`
|
|
157
214
|
|
|
158
215
|
```bash
|
|
159
216
|
cd example
|
|
@@ -170,6 +227,7 @@ Customize the focused and unfocused item styles with GPU-accelerated properties:
|
|
|
170
227
|
```ts
|
|
171
228
|
<HorizontalPicker
|
|
172
229
|
items={items}
|
|
230
|
+
containerHeight={53}
|
|
173
231
|
focusedTransformStyle={[{ scale: 1.2 }]}
|
|
174
232
|
unfocusedTransformStyle={[{ scale: 0.9 }]}
|
|
175
233
|
focusedOpacityStyle={1}
|
|
@@ -186,6 +244,7 @@ Customize the focused and unfocused item styles with GPU-accelerated properties:
|
|
|
186
244
|
| `items` | `PickerOption[]` | – | Array of options to display. Each option is an object with `label` and `value`. |
|
|
187
245
|
| `initialScrollIndex` | `number` | `0` | Index of the item initially selected. |
|
|
188
246
|
| `visibleItemCount` | `number` | `7` | Number of items visible on screen at once. |
|
|
247
|
+
| `containerHeight` | `number` | required | Required picker container height reserved before width is measured, preventing layout shift. |
|
|
189
248
|
| `onChange` | `(value: string \| number, index: number) => void` | – | Callback triggered when the selected item changes. |
|
|
190
249
|
| `focusedTransformStyle` | `ViewStyle['transform']` | `[{ scale: 1.15 }]` | Transform style applied to the focused item (GPU-accelerated). |
|
|
191
250
|
| `unfocusedTransformStyle` | `ViewStyle['transform']` | `[{ scale: 1 }]` | Transform style applied to unfocused items (GPU-accelerated). |
|
|
@@ -205,7 +264,7 @@ The component extends `FlatListPropsWithLayout`, so you can also pass any valid
|
|
|
205
264
|
- `showsHorizontalScrollIndicator` (default: `false`)
|
|
206
265
|
- `initialNumToRender` (default: `15`)
|
|
207
266
|
- `maxToRenderPerBatch` (default: `15`)
|
|
208
|
-
- `removeClippedSubviews` (default: `
|
|
267
|
+
- `removeClippedSubviews` (default: `Platform.OS !== 'android'`)
|
|
209
268
|
- `ref` to call `scrollToEnd`, `scrollToIndex`, `scrollToItem`, and `scrollToOffset`
|
|
210
269
|
|
|
211
270
|
## ⚡ Performance Notes
|
package/build/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface HorizontalPickerRef {
|
|
|
35
35
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
36
36
|
ref?: Ref<HorizontalPickerRef | null>;
|
|
37
37
|
items: PickerOption[];
|
|
38
|
+
containerHeight: number;
|
|
38
39
|
initialScrollIndex?: number;
|
|
39
40
|
visibleItemCount?: number;
|
|
40
41
|
onChange?: (value: PickerOption['value'], index: number) => void;
|
|
@@ -45,6 +46,6 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
45
46
|
pickerItemStyle?: ViewStyle;
|
|
46
47
|
pickerItemTextStyle?: TextStyle;
|
|
47
48
|
}
|
|
48
|
-
export declare function HorizontalPicker({ ref, items, initialScrollIndex, visibleItemCount, onChange, keyExtractor, scrollEventThrottle, decelerationRate, onLayout, showsHorizontalScrollIndicator, initialNumToRender, maxToRenderPerBatch, removeClippedSubviews, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, style, ...props }: HorizontalPickerProps): import("react").JSX.Element;
|
|
49
|
+
export declare function HorizontalPicker({ ref, items, containerHeight, initialScrollIndex, visibleItemCount, onChange, keyExtractor, scrollEventThrottle, decelerationRate, onLayout, showsHorizontalScrollIndicator, initialNumToRender, maxToRenderPerBatch, removeClippedSubviews, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, style, ...props }: HorizontalPickerProps): import("react").JSX.Element;
|
|
49
50
|
export {};
|
|
50
51
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAkD,MAAM,OAAO,CAAC;AACjF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAkD,MAAM,OAAO,CAAC;AACjF,OAAO,EAML,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAiB,EACf,KAAK,uBAAuB,EAO7B,MAAM,yBAAyB,CAAC;AAEjC,UAAU,aACR,SAAQ,IAAI,CACV,uBAAuB,CAAC,YAAY,CAAC,EACnC,KAAK,GACL,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,oBAAoB,GACpB,UAAU,GACV,eAAe,GACf,uBAAuB,GACvB,eAAe,CAClB;CAAG;AAEN,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9D,aAAa,EAAE,CAAC,MAAM,EAAE;QACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,YAAY,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,IAAI,EAAE,YAAY,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,cAAc,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjF;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,GAAG,CAAC,EAAE,GAAG,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACtC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,qBAAqB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAC/C,uBAAuB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,mBAAmB,CAAC,EAAE,SAAS,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,eAAe,EACf,kBAAsB,EACtB,gBAAoB,EACpB,QAAQ,EACR,YAAwD,EACxD,mBAAwB,EACxB,gBAAyB,EACzB,QAAQ,EACR,8BAAsC,EACtC,kBAAuB,EACvB,mBAAwB,EACxB,qBAAiD,EACjD,qBAAyC,EACzC,uBAAwC,EACxC,mBAAuB,EACvB,qBAA2B,EAC3B,eAAe,EACf,mBAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,qBAAqB,+BA6IvB"}
|
package/build/index.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { PixelRatio, Pressable, StyleSheet, View, } from 'react-native';
|
|
2
|
+
import { PixelRatio, Platform, Pressable, StyleSheet, View, } from 'react-native';
|
|
3
3
|
import Animated, { runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
|
|
4
|
-
export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleItemCount = 7, onChange, keyExtractor = (item, index) => `${item.value}-${index}`, scrollEventThrottle = 16, decelerationRate = 'fast', onLayout, showsHorizontalScrollIndicator = false, initialNumToRender = 15, maxToRenderPerBatch = 15, removeClippedSubviews =
|
|
4
|
+
export function HorizontalPicker({ ref, items, containerHeight, initialScrollIndex = 0, visibleItemCount = 7, onChange, keyExtractor = (item, index) => `${item.value}-${index}`, scrollEventThrottle = 16, decelerationRate = 'fast', onLayout, showsHorizontalScrollIndicator = false, initialNumToRender = 15, maxToRenderPerBatch = 15, removeClippedSubviews = Platform.OS !== 'android', focusedTransformStyle = [{ scale: 1.15 }], unfocusedTransformStyle = [{ scale: 1 }], focusedOpacityStyle = 1, unfocusedOpacityStyle = 0.2, pickerItemStyle, pickerItemTextStyle, style, ...props }) {
|
|
5
5
|
const listRef = useRef(null);
|
|
6
6
|
const [width, setWidth] = useState(0);
|
|
7
7
|
const currentIndex = useSharedValue(initialScrollIndex);
|
|
8
8
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
9
|
+
if (width <= 0) {
|
|
10
|
+
return {
|
|
11
|
+
itemWidth: 0,
|
|
12
|
+
paddingSide: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
9
15
|
const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);
|
|
10
16
|
const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);
|
|
11
17
|
return {
|
|
12
18
|
itemWidth: itemWidth,
|
|
13
19
|
paddingSide: paddingSide,
|
|
14
20
|
};
|
|
15
|
-
}, [
|
|
21
|
+
}, [visibleItemCount, width]);
|
|
22
|
+
const containerMinHeight = useMemo(() => {
|
|
23
|
+
if (!Number.isFinite(containerHeight)) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(containerHeight, 1);
|
|
27
|
+
}, [containerHeight]);
|
|
16
28
|
const snapOffsets = useMemo(() => {
|
|
29
|
+
if (itemWidth <= 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
17
32
|
return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));
|
|
18
33
|
}, [items, itemWidth]);
|
|
19
34
|
const handleOnChange = (index) => {
|
|
@@ -23,6 +38,9 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
23
38
|
}
|
|
24
39
|
};
|
|
25
40
|
const scrollToIndex = (index) => {
|
|
41
|
+
if (itemWidth <= 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
26
44
|
listRef.current?.scrollToOffset({
|
|
27
45
|
offset: index * itemWidth,
|
|
28
46
|
animated: true,
|
|
@@ -30,11 +48,17 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
30
48
|
};
|
|
31
49
|
const onScroll = useAnimatedScrollHandler({
|
|
32
50
|
onScroll: (e) => {
|
|
51
|
+
if (itemWidth <= 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
33
54
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
34
55
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
35
56
|
currentIndex.value = safeIndex;
|
|
36
57
|
},
|
|
37
58
|
onMomentumEnd: (e) => {
|
|
59
|
+
if (itemWidth <= 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
38
62
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
39
63
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
40
64
|
currentIndex.value = safeIndex;
|
|
@@ -47,16 +71,22 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
47
71
|
scrollToItem: (params) => listRef.current?.scrollToItem(params),
|
|
48
72
|
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
|
|
49
73
|
}), []);
|
|
50
|
-
return (<
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
return (<View style={[styles.container, { minHeight: containerMinHeight }]} onLayout={(e) => {
|
|
75
|
+
const nextWidth = e.nativeEvent.layout.width;
|
|
76
|
+
if (nextWidth <= 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (nextWidth === width) {
|
|
80
|
+
return;
|
|
53
81
|
}
|
|
54
|
-
setWidth(
|
|
55
|
-
}}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
setWidth(nextWidth);
|
|
83
|
+
}}>
|
|
84
|
+
{width > 0 && (<Animated.FlatList {...props} ref={listRef} horizontal={true} data={items} keyExtractor={keyExtractor} renderItem={({ item, index }) => (<PickerItem label={item.label} index={index} itemWidth={itemWidth} currentIndex={currentIndex} onPress={() => scrollToIndex(index)} focusedTransformStyle={focusedTransformStyle} unfocusedTransformStyle={unfocusedTransformStyle} focusedOpacityStyle={focusedOpacityStyle} unfocusedOpacityStyle={unfocusedOpacityStyle} pickerItemStyle={pickerItemStyle} pickerItemTextStyle={pickerItemTextStyle}/>)} onScroll={onScroll} scrollEventThrottle={scrollEventThrottle} decelerationRate={decelerationRate} onLayout={onLayout} showsHorizontalScrollIndicator={showsHorizontalScrollIndicator} snapToOffsets={snapOffsets} contentContainerStyle={{ paddingHorizontal: paddingSide }} getItemLayout={(_, index) => ({
|
|
85
|
+
length: itemWidth,
|
|
86
|
+
offset: itemWidth * index,
|
|
87
|
+
index,
|
|
88
|
+
})} initialScrollIndex={initialScrollIndex} initialNumToRender={initialNumToRender} maxToRenderPerBatch={maxToRenderPerBatch} removeClippedSubviews={removeClippedSubviews} style={style}/>)}
|
|
89
|
+
</View>);
|
|
60
90
|
}
|
|
61
91
|
function PickerItem({ label, index, itemWidth, currentIndex, onPress, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, }) {
|
|
62
92
|
const isFocused = useDerivedValue(() => currentIndex.value === index);
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type Ref, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport {\n type FlatList as NativeFlatList,\n PixelRatio,\n Pressable,\n StyleSheet,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type FlatListPropsWithLayout,\n type SharedValue,\n runOnJS,\n useAnimatedScrollHandler,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface FlatListProps\n extends Omit<\n FlatListPropsWithLayout<PickerOption>,\n | 'ref'\n | 'horizontal'\n | 'data'\n | 'renderItem'\n | 'initialScrollIndex'\n | 'onScroll'\n | 'snapToOffsets'\n | 'contentContainerStyle'\n | 'getItemLayout'\n > {}\n\nexport interface PickerOption {\n label: string;\n value: string | number;\n}\n\nexport interface PickerValues {\n index: number;\n value: PickerOption['value'];\n}\n\nexport interface HorizontalPickerRef {\n scrollToEnd: (params?: { animated?: boolean | null }) => void;\n scrollToIndex: (params: {\n animated?: boolean | null;\n index: number;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToItem: (params: {\n animated?: boolean | null;\n item: PickerOption;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToOffset: (params: { animated?: boolean | null; offset: number }) => void;\n}\n\nexport interface HorizontalPickerProps extends FlatListProps {\n ref?: Ref<HorizontalPickerRef | null>;\n items: PickerOption[];\n initialScrollIndex?: number;\n visibleItemCount?: number;\n onChange?: (value: PickerOption['value'], index: number) => void;\n focusedTransformStyle?: ViewStyle['transform'];\n unfocusedTransformStyle?: ViewStyle['transform'];\n focusedOpacityStyle?: number;\n unfocusedOpacityStyle?: number;\n pickerItemStyle?: ViewStyle;\n pickerItemTextStyle?: TextStyle;\n}\n\nexport function HorizontalPicker({\n ref,\n items,\n initialScrollIndex = 0,\n visibleItemCount = 7,\n onChange,\n keyExtractor = (item, index) => `${item.value}-${index}`,\n scrollEventThrottle = 16,\n decelerationRate = 'fast',\n onLayout,\n showsHorizontalScrollIndicator = false,\n initialNumToRender = 15,\n maxToRenderPerBatch = 15,\n removeClippedSubviews = true,\n focusedTransformStyle = [{ scale: 1.15 }],\n unfocusedTransformStyle = [{ scale: 1 }],\n focusedOpacityStyle = 1,\n unfocusedOpacityStyle = 0.2,\n pickerItemStyle,\n pickerItemTextStyle,\n style,\n ...props\n}: HorizontalPickerProps) {\n const listRef = useRef<NativeFlatList<PickerOption> | null>(null);\n const [width, setWidth] = useState<number>(0);\n\n const currentIndex = useSharedValue<number>(initialScrollIndex);\n\n const { itemWidth, paddingSide } = useMemo(() => {\n const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);\n const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);\n return {\n itemWidth: itemWidth,\n paddingSide: paddingSide,\n };\n }, [width, visibleItemCount]);\n\n const snapOffsets = useMemo(() => {\n return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));\n }, [items, itemWidth]);\n\n const handleOnChange = (index: number) => {\n const item = items[index];\n if (item) {\n onChange?.(item.value, index);\n }\n };\n\n const scrollToIndex = (index: number) => {\n listRef.current?.scrollToOffset({\n offset: index * itemWidth,\n animated: true,\n });\n };\n\n const onScroll = useAnimatedScrollHandler({\n onScroll: (e) => {\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n },\n onMomentumEnd: (e) => {\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n runOnJS(handleOnChange)(safeIndex);\n },\n });\n\n useImperativeHandle(\n ref,\n () => ({\n scrollToEnd: (params) => listRef.current?.scrollToEnd(params),\n scrollToIndex: (params) => listRef.current?.scrollToIndex(params),\n scrollToItem: (params) => listRef.current?.scrollToItem(params),\n scrollToOffset: (params) => listRef.current?.scrollToOffset(params),\n }),\n [],\n );\n\n return (\n <Animated.FlatList\n {...props}\n ref={listRef}\n horizontal={true}\n data={items}\n keyExtractor={keyExtractor}\n renderItem={({ item, index }) => (\n <PickerItem\n label={item.label}\n index={index}\n itemWidth={itemWidth}\n currentIndex={currentIndex}\n onPress={() => scrollToIndex(index)}\n focusedTransformStyle={focusedTransformStyle}\n unfocusedTransformStyle={unfocusedTransformStyle}\n focusedOpacityStyle={focusedOpacityStyle}\n unfocusedOpacityStyle={unfocusedOpacityStyle}\n pickerItemStyle={pickerItemStyle}\n pickerItemTextStyle={pickerItemTextStyle}\n />\n )}\n onScroll={onScroll}\n scrollEventThrottle={scrollEventThrottle}\n decelerationRate={decelerationRate}\n onLayout={(e) => {\n if (typeof onLayout === 'function') {\n onLayout(e);\n }\n setWidth(e.nativeEvent.layout.width);\n }}\n showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}\n snapToOffsets={snapOffsets}\n contentContainerStyle={{ paddingHorizontal: paddingSide }}\n getItemLayout={(_, index) => ({\n length: itemWidth,\n offset: itemWidth * index,\n index,\n })}\n initialScrollIndex={initialScrollIndex}\n initialNumToRender={initialNumToRender}\n maxToRenderPerBatch={maxToRenderPerBatch}\n removeClippedSubviews={removeClippedSubviews}\n style={[styles.container, style]}\n />\n );\n}\ninterface PickerItemProps\n extends Pick<\n HorizontalPickerProps,\n | 'focusedTransformStyle'\n | 'unfocusedTransformStyle'\n | 'focusedOpacityStyle'\n | 'unfocusedOpacityStyle'\n | 'pickerItemStyle'\n | 'pickerItemTextStyle'\n > {\n label: string;\n index: number;\n itemWidth: number;\n currentIndex: SharedValue<number>;\n onPress: () => void;\n}\n\nfunction PickerItem({\n label,\n index,\n itemWidth,\n currentIndex,\n onPress,\n focusedTransformStyle,\n unfocusedTransformStyle,\n focusedOpacityStyle,\n unfocusedOpacityStyle,\n pickerItemStyle,\n pickerItemTextStyle,\n}: PickerItemProps) {\n const isFocused = useDerivedValue(() => currentIndex.value === index);\n\n const animatedStyle = useAnimatedStyle(() => {\n return {\n transform: isFocused.value ? focusedTransformStyle : unfocusedTransformStyle,\n opacity: isFocused.value ? focusedOpacityStyle : unfocusedOpacityStyle,\n };\n }, [index]);\n\n return (\n <Pressable onPress={onPress}>\n <View style={[styles.itemContainer, { width: itemWidth }, pickerItemStyle]}>\n <Animated.Text style={[styles.itemText, animatedStyle, pickerItemTextStyle]}>{label}</Animated.Text>\n </View>\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n },\n itemContainer: {\n justifyContent: 'center',\n alignItems: 'center',\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#000000',\n },\n});\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type Ref, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport {\n type FlatList as NativeFlatList,\n PixelRatio,\n Platform,\n Pressable,\n StyleSheet,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type FlatListPropsWithLayout,\n type SharedValue,\n runOnJS,\n useAnimatedScrollHandler,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface FlatListProps\n extends Omit<\n FlatListPropsWithLayout<PickerOption>,\n | 'ref'\n | 'horizontal'\n | 'data'\n | 'renderItem'\n | 'initialScrollIndex'\n | 'onScroll'\n | 'snapToOffsets'\n | 'contentContainerStyle'\n | 'getItemLayout'\n > {}\n\nexport interface PickerOption {\n label: string;\n value: string | number;\n}\n\nexport interface PickerValues {\n index: number;\n value: PickerOption['value'];\n}\n\nexport interface HorizontalPickerRef {\n scrollToEnd: (params?: { animated?: boolean | null }) => void;\n scrollToIndex: (params: {\n animated?: boolean | null;\n index: number;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToItem: (params: {\n animated?: boolean | null;\n item: PickerOption;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToOffset: (params: { animated?: boolean | null; offset: number }) => void;\n}\n\nexport interface HorizontalPickerProps extends FlatListProps {\n ref?: Ref<HorizontalPickerRef | null>;\n items: PickerOption[];\n containerHeight: number;\n initialScrollIndex?: number;\n visibleItemCount?: number;\n onChange?: (value: PickerOption['value'], index: number) => void;\n focusedTransformStyle?: ViewStyle['transform'];\n unfocusedTransformStyle?: ViewStyle['transform'];\n focusedOpacityStyle?: number;\n unfocusedOpacityStyle?: number;\n pickerItemStyle?: ViewStyle;\n pickerItemTextStyle?: TextStyle;\n}\n\nexport function HorizontalPicker({\n ref,\n items,\n containerHeight,\n initialScrollIndex = 0,\n visibleItemCount = 7,\n onChange,\n keyExtractor = (item, index) => `${item.value}-${index}`,\n scrollEventThrottle = 16,\n decelerationRate = 'fast',\n onLayout,\n showsHorizontalScrollIndicator = false,\n initialNumToRender = 15,\n maxToRenderPerBatch = 15,\n removeClippedSubviews = Platform.OS !== 'android',\n focusedTransformStyle = [{ scale: 1.15 }],\n unfocusedTransformStyle = [{ scale: 1 }],\n focusedOpacityStyle = 1,\n unfocusedOpacityStyle = 0.2,\n pickerItemStyle,\n pickerItemTextStyle,\n style,\n ...props\n}: HorizontalPickerProps) {\n const listRef = useRef<NativeFlatList<PickerOption> | null>(null);\n const [width, setWidth] = useState<number>(0);\n\n const currentIndex = useSharedValue<number>(initialScrollIndex);\n\n const { itemWidth, paddingSide } = useMemo(() => {\n if (width <= 0) {\n return {\n itemWidth: 0,\n paddingSide: 0,\n };\n }\n\n const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);\n const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);\n return {\n itemWidth: itemWidth,\n paddingSide: paddingSide,\n };\n }, [visibleItemCount, width]);\n\n const containerMinHeight = useMemo(() => {\n if (!Number.isFinite(containerHeight)) {\n return 1;\n }\n return Math.max(containerHeight, 1);\n }, [containerHeight]);\n\n const snapOffsets = useMemo(() => {\n if (itemWidth <= 0) {\n return [];\n }\n return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));\n }, [items, itemWidth]);\n\n const handleOnChange = (index: number) => {\n const item = items[index];\n if (item) {\n onChange?.(item.value, index);\n }\n };\n\n const scrollToIndex = (index: number) => {\n if (itemWidth <= 0) {\n return;\n }\n listRef.current?.scrollToOffset({\n offset: index * itemWidth,\n animated: true,\n });\n };\n\n const onScroll = useAnimatedScrollHandler({\n onScroll: (e) => {\n if (itemWidth <= 0) {\n return;\n }\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n },\n onMomentumEnd: (e) => {\n if (itemWidth <= 0) {\n return;\n }\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n runOnJS(handleOnChange)(safeIndex);\n },\n });\n\n useImperativeHandle(\n ref,\n () => ({\n scrollToEnd: (params) => listRef.current?.scrollToEnd(params),\n scrollToIndex: (params) => listRef.current?.scrollToIndex(params),\n scrollToItem: (params) => listRef.current?.scrollToItem(params),\n scrollToOffset: (params) => listRef.current?.scrollToOffset(params),\n }),\n [],\n );\n\n return (\n <View\n style={[styles.container, { minHeight: containerMinHeight }]}\n onLayout={(e) => {\n const nextWidth = e.nativeEvent.layout.width;\n if (nextWidth <= 0) {\n return;\n }\n if (nextWidth === width) {\n return;\n }\n setWidth(nextWidth);\n }}\n >\n {width > 0 && (\n <Animated.FlatList\n {...props}\n ref={listRef}\n horizontal={true}\n data={items}\n keyExtractor={keyExtractor}\n renderItem={({ item, index }) => (\n <PickerItem\n label={item.label}\n index={index}\n itemWidth={itemWidth}\n currentIndex={currentIndex}\n onPress={() => scrollToIndex(index)}\n focusedTransformStyle={focusedTransformStyle}\n unfocusedTransformStyle={unfocusedTransformStyle}\n focusedOpacityStyle={focusedOpacityStyle}\n unfocusedOpacityStyle={unfocusedOpacityStyle}\n pickerItemStyle={pickerItemStyle}\n pickerItemTextStyle={pickerItemTextStyle}\n />\n )}\n onScroll={onScroll}\n scrollEventThrottle={scrollEventThrottle}\n decelerationRate={decelerationRate}\n onLayout={onLayout}\n showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}\n snapToOffsets={snapOffsets}\n contentContainerStyle={{ paddingHorizontal: paddingSide }}\n getItemLayout={(_, index) => ({\n length: itemWidth,\n offset: itemWidth * index,\n index,\n })}\n initialScrollIndex={initialScrollIndex}\n initialNumToRender={initialNumToRender}\n maxToRenderPerBatch={maxToRenderPerBatch}\n removeClippedSubviews={removeClippedSubviews}\n style={style}\n />\n )}\n </View>\n );\n}\ninterface PickerItemProps\n extends Pick<\n HorizontalPickerProps,\n | 'focusedTransformStyle'\n | 'unfocusedTransformStyle'\n | 'focusedOpacityStyle'\n | 'unfocusedOpacityStyle'\n | 'pickerItemStyle'\n | 'pickerItemTextStyle'\n > {\n label: string;\n index: number;\n itemWidth: number;\n currentIndex: SharedValue<number>;\n onPress: () => void;\n}\n\nfunction PickerItem({\n label,\n index,\n itemWidth,\n currentIndex,\n onPress,\n focusedTransformStyle,\n unfocusedTransformStyle,\n focusedOpacityStyle,\n unfocusedOpacityStyle,\n pickerItemStyle,\n pickerItemTextStyle,\n}: PickerItemProps) {\n const isFocused = useDerivedValue(() => currentIndex.value === index);\n\n const animatedStyle = useAnimatedStyle(() => {\n return {\n transform: isFocused.value ? focusedTransformStyle : unfocusedTransformStyle,\n opacity: isFocused.value ? focusedOpacityStyle : unfocusedOpacityStyle,\n };\n }, [index]);\n\n return (\n <Pressable onPress={onPress}>\n <View style={[styles.itemContainer, { width: itemWidth }, pickerItemStyle]}>\n <Animated.Text style={[styles.itemText, animatedStyle, pickerItemTextStyle]}>{label}</Animated.Text>\n </View>\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n },\n itemContainer: {\n justifyContent: 'center',\n alignItems: 'center',\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#000000',\n },\n});\n"]}
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { type Ref, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
type FlatList as NativeFlatList,
|
|
4
4
|
PixelRatio,
|
|
5
|
+
Platform,
|
|
5
6
|
Pressable,
|
|
6
7
|
StyleSheet,
|
|
7
8
|
type TextStyle,
|
|
@@ -62,6 +63,7 @@ export interface HorizontalPickerRef {
|
|
|
62
63
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
63
64
|
ref?: Ref<HorizontalPickerRef | null>;
|
|
64
65
|
items: PickerOption[];
|
|
66
|
+
containerHeight: number;
|
|
65
67
|
initialScrollIndex?: number;
|
|
66
68
|
visibleItemCount?: number;
|
|
67
69
|
onChange?: (value: PickerOption['value'], index: number) => void;
|
|
@@ -76,6 +78,7 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
76
78
|
export function HorizontalPicker({
|
|
77
79
|
ref,
|
|
78
80
|
items,
|
|
81
|
+
containerHeight,
|
|
79
82
|
initialScrollIndex = 0,
|
|
80
83
|
visibleItemCount = 7,
|
|
81
84
|
onChange,
|
|
@@ -86,7 +89,7 @@ export function HorizontalPicker({
|
|
|
86
89
|
showsHorizontalScrollIndicator = false,
|
|
87
90
|
initialNumToRender = 15,
|
|
88
91
|
maxToRenderPerBatch = 15,
|
|
89
|
-
removeClippedSubviews =
|
|
92
|
+
removeClippedSubviews = Platform.OS !== 'android',
|
|
90
93
|
focusedTransformStyle = [{ scale: 1.15 }],
|
|
91
94
|
unfocusedTransformStyle = [{ scale: 1 }],
|
|
92
95
|
focusedOpacityStyle = 1,
|
|
@@ -102,15 +105,32 @@ export function HorizontalPicker({
|
|
|
102
105
|
const currentIndex = useSharedValue<number>(initialScrollIndex);
|
|
103
106
|
|
|
104
107
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
108
|
+
if (width <= 0) {
|
|
109
|
+
return {
|
|
110
|
+
itemWidth: 0,
|
|
111
|
+
paddingSide: 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);
|
|
106
116
|
const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);
|
|
107
117
|
return {
|
|
108
118
|
itemWidth: itemWidth,
|
|
109
119
|
paddingSide: paddingSide,
|
|
110
120
|
};
|
|
111
|
-
}, [
|
|
121
|
+
}, [visibleItemCount, width]);
|
|
122
|
+
|
|
123
|
+
const containerMinHeight = useMemo(() => {
|
|
124
|
+
if (!Number.isFinite(containerHeight)) {
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
return Math.max(containerHeight, 1);
|
|
128
|
+
}, [containerHeight]);
|
|
112
129
|
|
|
113
130
|
const snapOffsets = useMemo(() => {
|
|
131
|
+
if (itemWidth <= 0) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
114
134
|
return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));
|
|
115
135
|
}, [items, itemWidth]);
|
|
116
136
|
|
|
@@ -122,6 +142,9 @@ export function HorizontalPicker({
|
|
|
122
142
|
};
|
|
123
143
|
|
|
124
144
|
const scrollToIndex = (index: number) => {
|
|
145
|
+
if (itemWidth <= 0) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
125
148
|
listRef.current?.scrollToOffset({
|
|
126
149
|
offset: index * itemWidth,
|
|
127
150
|
animated: true,
|
|
@@ -130,11 +153,17 @@ export function HorizontalPicker({
|
|
|
130
153
|
|
|
131
154
|
const onScroll = useAnimatedScrollHandler({
|
|
132
155
|
onScroll: (e) => {
|
|
156
|
+
if (itemWidth <= 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
133
159
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
134
160
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
135
161
|
currentIndex.value = safeIndex;
|
|
136
162
|
},
|
|
137
163
|
onMomentumEnd: (e) => {
|
|
164
|
+
if (itemWidth <= 0) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
138
167
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
139
168
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
140
169
|
currentIndex.value = safeIndex;
|
|
@@ -154,50 +183,61 @@ export function HorizontalPicker({
|
|
|
154
183
|
);
|
|
155
184
|
|
|
156
185
|
return (
|
|
157
|
-
<
|
|
158
|
-
{
|
|
159
|
-
ref={listRef}
|
|
160
|
-
horizontal={true}
|
|
161
|
-
data={items}
|
|
162
|
-
keyExtractor={keyExtractor}
|
|
163
|
-
renderItem={({ item, index }) => (
|
|
164
|
-
<PickerItem
|
|
165
|
-
label={item.label}
|
|
166
|
-
index={index}
|
|
167
|
-
itemWidth={itemWidth}
|
|
168
|
-
currentIndex={currentIndex}
|
|
169
|
-
onPress={() => scrollToIndex(index)}
|
|
170
|
-
focusedTransformStyle={focusedTransformStyle}
|
|
171
|
-
unfocusedTransformStyle={unfocusedTransformStyle}
|
|
172
|
-
focusedOpacityStyle={focusedOpacityStyle}
|
|
173
|
-
unfocusedOpacityStyle={unfocusedOpacityStyle}
|
|
174
|
-
pickerItemStyle={pickerItemStyle}
|
|
175
|
-
pickerItemTextStyle={pickerItemTextStyle}
|
|
176
|
-
/>
|
|
177
|
-
)}
|
|
178
|
-
onScroll={onScroll}
|
|
179
|
-
scrollEventThrottle={scrollEventThrottle}
|
|
180
|
-
decelerationRate={decelerationRate}
|
|
186
|
+
<View
|
|
187
|
+
style={[styles.container, { minHeight: containerMinHeight }]}
|
|
181
188
|
onLayout={(e) => {
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
const nextWidth = e.nativeEvent.layout.width;
|
|
190
|
+
if (nextWidth <= 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (nextWidth === width) {
|
|
194
|
+
return;
|
|
184
195
|
}
|
|
185
|
-
setWidth(
|
|
196
|
+
setWidth(nextWidth);
|
|
186
197
|
}}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
>
|
|
199
|
+
{width > 0 && (
|
|
200
|
+
<Animated.FlatList
|
|
201
|
+
{...props}
|
|
202
|
+
ref={listRef}
|
|
203
|
+
horizontal={true}
|
|
204
|
+
data={items}
|
|
205
|
+
keyExtractor={keyExtractor}
|
|
206
|
+
renderItem={({ item, index }) => (
|
|
207
|
+
<PickerItem
|
|
208
|
+
label={item.label}
|
|
209
|
+
index={index}
|
|
210
|
+
itemWidth={itemWidth}
|
|
211
|
+
currentIndex={currentIndex}
|
|
212
|
+
onPress={() => scrollToIndex(index)}
|
|
213
|
+
focusedTransformStyle={focusedTransformStyle}
|
|
214
|
+
unfocusedTransformStyle={unfocusedTransformStyle}
|
|
215
|
+
focusedOpacityStyle={focusedOpacityStyle}
|
|
216
|
+
unfocusedOpacityStyle={unfocusedOpacityStyle}
|
|
217
|
+
pickerItemStyle={pickerItemStyle}
|
|
218
|
+
pickerItemTextStyle={pickerItemTextStyle}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
onScroll={onScroll}
|
|
222
|
+
scrollEventThrottle={scrollEventThrottle}
|
|
223
|
+
decelerationRate={decelerationRate}
|
|
224
|
+
onLayout={onLayout}
|
|
225
|
+
showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
|
|
226
|
+
snapToOffsets={snapOffsets}
|
|
227
|
+
contentContainerStyle={{ paddingHorizontal: paddingSide }}
|
|
228
|
+
getItemLayout={(_, index) => ({
|
|
229
|
+
length: itemWidth,
|
|
230
|
+
offset: itemWidth * index,
|
|
231
|
+
index,
|
|
232
|
+
})}
|
|
233
|
+
initialScrollIndex={initialScrollIndex}
|
|
234
|
+
initialNumToRender={initialNumToRender}
|
|
235
|
+
maxToRenderPerBatch={maxToRenderPerBatch}
|
|
236
|
+
removeClippedSubviews={removeClippedSubviews}
|
|
237
|
+
style={style}
|
|
238
|
+
/>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
201
241
|
);
|
|
202
242
|
}
|
|
203
243
|
interface PickerItemProps
|