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 CHANGED
@@ -39,65 +39,117 @@ Make sure to follow the additional setup instructions for Reanimated in the [off
39
39
  ![expo-horizontal-picker demo](https://raw.githubusercontent.com/fe-dudu/expo-horizontal-picker/main/assets/demo.gif)
40
40
 
41
41
  ```ts
42
- import { HorizontalPicker, type HorizontalPickerRef, type PickerValues } from 'expo-horizontal-picker';
42
+ import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
43
43
  import { useRef, useState } from 'react';
44
- import { StyleSheet, Text, View } from 'react-native';
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
- return (
60
- <View style={styles.container}>
61
- <Text style={styles.title}>Sync · 7 visible → {selected.value}</Text>
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
- <HorizontalPicker
64
- ref={firstPickerRef}
65
- items={numberItems}
66
- initialScrollIndex={499}
67
- visibleItemCount={7}
68
- onChange={(value, index) => {
69
- setSelected({ index, value });
70
- secondPickerRef.current?.scrollToIndex({ index, animated: true });
71
- }}
72
- pickerItemStyle={styles.pickerItem}
73
- />
74
-
75
- <HorizontalPicker
76
- ref={secondPickerRef}
77
- items={numberItems}
78
- initialScrollIndex={499}
79
- visibleItemCount={7}
80
- onChange={(value, index) => {
81
- setSelected({ index, value });
82
- firstPickerRef.current?.scrollToIndex({ index, animated: true });
83
- }}
84
- pickerItemStyle={styles.pickerItem}
85
- />
86
- </View>
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
- marginBottom: 12,
99
- fontSize: 18,
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 mirrors the README demo and includes ref-driven scroll controls.
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: `true`)
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
@@ -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,EAKL,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,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,kBAAsB,EACtB,gBAAoB,EACpB,QAAQ,EACR,YAAwD,EACxD,mBAAwB,EACxB,gBAAyB,EACzB,QAAQ,EACR,8BAAsC,EACtC,kBAAuB,EACvB,mBAAwB,EACxB,qBAA4B,EAC5B,qBAAyC,EACzC,uBAAwC,EACxC,mBAAuB,EACvB,qBAA2B,EAC3B,eAAe,EACf,mBAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,qBAAqB,+BAwGvB"}
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 = true, focusedTransformStyle = [{ scale: 1.15 }], unfocusedTransformStyle = [{ scale: 1 }], focusedOpacityStyle = 1, unfocusedOpacityStyle = 0.2, pickerItemStyle, pickerItemTextStyle, style, ...props }) {
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
- }, [width, visibleItemCount]);
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 (<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={(e) => {
51
- if (typeof onLayout === 'function') {
52
- onLayout(e);
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(e.nativeEvent.layout.width);
55
- }} showsHorizontalScrollIndicator={showsHorizontalScrollIndicator} snapToOffsets={snapOffsets} contentContainerStyle={{ paddingHorizontal: paddingSide }} getItemLayout={(_, index) => ({
56
- length: itemWidth,
57
- offset: itemWidth * index,
58
- index,
59
- })} initialScrollIndex={initialScrollIndex} initialNumToRender={initialNumToRender} maxToRenderPerBatch={maxToRenderPerBatch} removeClippedSubviews={removeClippedSubviews} style={[styles.container, style]}/>);
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);
@@ -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":"AAAA,OAAO,EAAY,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjF,OAAO,EAEL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,UAAU,EAEV,IAAI,GAEL,MAAM,cAAc,CAAC;AACtB,OAAO,QAAQ,EAAE,EAGf,OAAO,EACP,wBAAwB,EACxB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,yBAAyB,CAAC;AA0DjC,MAAM,UAAU,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,eAAe,EACf,kBAAkB,GAAG,CAAC,EACtB,gBAAgB,GAAG,CAAC,EACpB,QAAQ,EACR,YAAY,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,EAAE,EACxD,mBAAmB,GAAG,EAAE,EACxB,gBAAgB,GAAG,MAAM,EACzB,QAAQ,EACR,8BAA8B,GAAG,KAAK,EACtC,kBAAkB,GAAG,EAAE,EACvB,mBAAmB,GAAG,EAAE,EACxB,qBAAqB,GAAG,QAAQ,CAAC,EAAE,KAAK,SAAS,EACjD,qBAAqB,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EACzC,uBAAuB,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EACxC,mBAAmB,GAAG,CAAC,EACvB,qBAAqB,GAAG,GAAG,EAC3B,eAAe,EACf,mBAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACc;IACtB,MAAM,OAAO,GAAG,MAAM,CAAsC,IAAI,CAAC,CAAC;IAClE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAS,CAAC,CAAC,CAAC;IAE9C,MAAM,YAAY,GAAG,cAAc,CAAS,kBAAkB,CAAC,CAAC;IAEhE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9C,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACf,OAAO;gBACL,SAAS,EAAE,CAAC;gBACZ,WAAW,EAAE,CAAC;aACf,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,mBAAmB,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC;QAC3E,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,KAAK,GAAG,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC;QAC9E,OAAO;YACL,SAAS,EAAE,SAAS;YACpB,WAAW,EAAE,WAAW;SACzB,CAAC;IACJ,CAAC,EAAE,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC;IAE9B,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,EAAE;QACtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;YACtC,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC;IACtC,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IAEtB,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC;IACpF,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IAEvB,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,EAAE;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,IAAI,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,aAAa,GAAG,CAAC,KAAa,EAAE,EAAE;QACtC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;YACnB,OAAO;QACT,CAAC;QACD,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC;YAC9B,MAAM,EAAE,KAAK,GAAG,SAAS;YACzB,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,wBAAwB,CAAC;QACxC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;YACd,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;YAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;YACpE,YAAY,CAAC,KAAK,GAAG,SAAS,CAAC;QACjC,CAAC;QACD,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE;YACnB,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;YAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;YACpE,YAAY,CAAC,KAAK,GAAG,SAAS,CAAC;YAC/B,OAAO,CAAC,cAAc,CAAC,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC;KACF,CAAC,CAAC;IAEH,mBAAmB,CACjB,GAAG,EACH,GAAG,EAAE,CAAC,CAAC;QACL,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC;QAC7D,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC;QACjE,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC;QAC/D,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC;KACpE,CAAC,EACF,EAAE,CACH,CAAC;IAEF,OAAO,CACL,CAAC,IAAI,CACH,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAC7D,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE;YACd,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC;YAC7C,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;gBACnB,OAAO;YACT,CAAC;YACD,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;gBACxB,OAAO;YACT,CAAC;YACD,QAAQ,CAAC,SAAS,CAAC,CAAC;QACtB,CAAC,CAAC,CAEF;MAAA,CAAC,KAAK,GAAG,CAAC,IAAI,CACZ,CAAC,QAAQ,CAAC,QAAQ,CAChB,IAAI,KAAK,CAAC,CACV,GAAG,CAAC,CAAC,OAAO,CAAC,CACb,UAAU,CAAC,CAAC,IAAI,CAAC,CACjB,IAAI,CAAC,CAAC,KAAK,CAAC,CACZ,YAAY,CAAC,CAAC,YAAY,CAAC,CAC3B,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAC/B,CAAC,UAAU,CACT,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAClB,KAAK,CAAC,CAAC,KAAK,CAAC,CACb,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,YAAY,CAAC,CAAC,YAAY,CAAC,CAC3B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CACpC,qBAAqB,CAAC,CAAC,qBAAqB,CAAC,CAC7C,uBAAuB,CAAC,CAAC,uBAAuB,CAAC,CACjD,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,CACzC,qBAAqB,CAAC,CAAC,qBAAqB,CAAC,CAC7C,eAAe,CAAC,CAAC,eAAe,CAAC,CACjC,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,EACzC,CACH,CAAC,CACF,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,CACzC,gBAAgB,CAAC,CAAC,gBAAgB,CAAC,CACnC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,8BAA8B,CAAC,CAAC,8BAA8B,CAAC,CAC/D,aAAa,CAAC,CAAC,WAAW,CAAC,CAC3B,qBAAqB,CAAC,CAAC,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAC,CAC1D,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC5B,MAAM,EAAE,SAAS;gBACjB,MAAM,EAAE,SAAS,GAAG,KAAK;gBACzB,KAAK;aACN,CAAC,CAAC,CACH,kBAAkB,CAAC,CAAC,kBAAkB,CAAC,CACvC,kBAAkB,CAAC,CAAC,kBAAkB,CAAC,CACvC,mBAAmB,CAAC,CAAC,mBAAmB,CAAC,CACzC,qBAAqB,CAAC,CAAC,qBAAqB,CAAC,CAC7C,KAAK,CAAC,CAAC,KAAK,CAAC,EACb,CACH,CACH;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC;AAkBD,SAAS,UAAU,CAAC,EAClB,KAAK,EACL,KAAK,EACL,SAAS,EACT,YAAY,EACZ,OAAO,EACP,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,GACH;IAChB,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;IAEtE,MAAM,aAAa,GAAG,gBAAgB,CAAC,GAAG,EAAE;QAC1C,OAAO;YACL,SAAS,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,uBAAuB;YAC5E,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB;SACvE,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,OAAO,CACL,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAC1B;MAAA,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,eAAe,CAAC,CAAC,CACzE;QAAA,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,IAAI,CACrG;MAAA,EAAE,IAAI,CACR;IAAA,EAAE,SAAS,CAAC,CACb,CAAC;AACJ,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAC/B,SAAS,EAAE;QACT,KAAK,EAAE,MAAM;KACd;IACD,aAAa,EAAE;QACb,cAAc,EAAE,QAAQ;QACxB,UAAU,EAAE,QAAQ;KACrB;IACD,QAAQ,EAAE;QACR,QAAQ,EAAE,EAAE;QACZ,UAAU,EAAE,KAAK;QACjB,KAAK,EAAE,SAAS;KACjB;CACF,CAAC,CAAC","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-horizontal-picker",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A performant horizontal picker component for React Native and Expo apps",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
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 = true,
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
- }, [width, visibleItemCount]);
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
- <Animated.FlatList
158
- {...props}
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
- if (typeof onLayout === 'function') {
183
- onLayout(e);
189
+ const nextWidth = e.nativeEvent.layout.width;
190
+ if (nextWidth <= 0) {
191
+ return;
192
+ }
193
+ if (nextWidth === width) {
194
+ return;
184
195
  }
185
- setWidth(e.nativeEvent.layout.width);
196
+ setWidth(nextWidth);
186
197
  }}
187
- showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
188
- snapToOffsets={snapOffsets}
189
- contentContainerStyle={{ paddingHorizontal: paddingSide }}
190
- getItemLayout={(_, index) => ({
191
- length: itemWidth,
192
- offset: itemWidth * index,
193
- index,
194
- })}
195
- initialScrollIndex={initialScrollIndex}
196
- initialNumToRender={initialNumToRender}
197
- maxToRenderPerBatch={maxToRenderPerBatch}
198
- removeClippedSubviews={removeClippedSubviews}
199
- style={[styles.container, style]}
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