expo-horizontal-picker 0.2.0 → 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,94 +39,178 @@ 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 { useRef } from 'react';
43
42
  import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
44
- import { Button, View } from 'react-native';
43
+ import { useRef, useState } from 'react';
44
+ import { ScrollView, StyleSheet, Text } from 'react-native';
45
+ import { SafeAreaView } from 'react-native-safe-area-context';
46
+
47
+ const numberItems = Array.from({ length: 600 }, (_, i) => ({
48
+ label: `${i + 1}`,
49
+ value: i + 1,
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;
45
68
 
46
69
  export default function App() {
47
- const pickerRef = useRef<HorizontalPickerRef | null>(null);
70
+ const firstPickerRef = useRef<HorizontalPickerRef | null>(null);
71
+ const secondPickerRef = useRef<HorizontalPickerRef | null>(null);
72
+
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 });
48
78
 
49
79
  return (
50
- <View style={styles.container}>
51
- <View>
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
+ />
52
97
  <HorizontalPicker
53
- items={Array.from({ length: 1000 }, (_, i) => ({
54
- label: `${i + 1}`,
55
- value: i + 1,
56
- }))}
98
+ ref={secondPickerRef}
99
+ items={numberItems}
100
+ containerHeight={pickerContainerHeight}
57
101
  initialScrollIndex={499}
58
102
  visibleItemCount={7}
103
+ onChange={(value, index) => {
104
+ setSelectedSecond({ index, value });
105
+ firstPickerRef.current?.scrollToIndex({ index, animated: true });
106
+ }}
107
+ pickerItemStyle={styles.pickerItem}
59
108
  />
60
109
 
110
+ {/* 5 / 3 / 1 visible items */}
61
111
  <HorizontalPicker
62
- items={Array.from({ length: 20 }, (_, i) => ({
63
- label: `${i + 1}k`,
64
- value: (i + 1) * 1000,
65
- }))}
112
+ items={thousandItems}
113
+ containerHeight={pickerContainerHeight}
66
114
  initialScrollIndex={9}
67
115
  visibleItemCount={5}
116
+ onChange={(value, index) => setSelectedThird({ index, value })}
117
+ pickerItemStyle={styles.pickerItem}
68
118
  />
69
-
70
119
  <HorizontalPicker
71
- items={Array.from({ length: 24 }, (_, i) => ({
72
- label: `${i + 1}h`,
73
- value: i + 1,
74
- }))}
75
- ref={pickerRef}
120
+ items={hourItems}
121
+ containerHeight={pickerContainerHeight}
76
122
  initialScrollIndex={11}
77
123
  visibleItemCount={3}
124
+ onChange={(value, index) => setSelectedFourth({ index, value })}
125
+ pickerItemStyle={styles.pickerItem}
78
126
  />
79
-
80
- <Button
81
- title="Jump to 24h"
82
- onPress={() => pickerRef.current?.scrollToEnd({ animated: true })}
83
- />
84
-
85
127
  <HorizontalPicker
86
- items={Array.from({ length: 5 }, (_, i) => ({
87
- label: `${(i + 1) * 10000}`,
88
- value: (i + 1) * 10000,
89
- }))}
128
+ items={largeNumberItems}
129
+ containerHeight={pickerContainerHeight}
90
130
  initialScrollIndex={2}
91
131
  visibleItemCount={1}
132
+ onChange={(value, index) => setSelectedFifth({ index, value })}
133
+ pickerItemStyle={styles.pickerItem}
92
134
  />
93
- </View>
94
- </View>
135
+ </ScrollView>
136
+ </SafeAreaView>
95
137
  );
96
138
  }
97
139
 
98
- const styles = {
140
+ const styles = StyleSheet.create({
99
141
  container: {
100
142
  flex: 1,
101
- backgroundColor: '#eee',
143
+ backgroundColor: '#eeeeee',
144
+ },
145
+ content: {
146
+ paddingHorizontal: 20,
147
+ paddingVertical: 12,
148
+ gap: 16,
149
+ },
150
+ title: {
151
+ fontSize: 28,
152
+ fontWeight: '800',
153
+ color: '#111111',
102
154
  },
103
- };
155
+ pickerItem: {
156
+ paddingVertical: 20,
157
+ },
158
+ });
104
159
  ```
105
160
 
106
161
  ## Ref Usage
107
162
 
108
163
  Pass a ref when you need the picker scroll methods: `scrollToEnd`, `scrollToIndex`, `scrollToItem`, or `scrollToOffset`.
109
164
 
165
+ The picker is intentionally stateful around its own scroll position. A ref is meant for imperative coordination, such as keeping two pickers visually in sync or jumping to a specific item from another control. If you choose that pattern, keep any mirrored app state in the parent and update it alongside the ref call, just like the example below.
166
+
110
167
  ```ts
111
- import { useRef } from 'react';
112
- import { Button } from 'react-native';
113
- import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
168
+ import { HorizontalPicker, type HorizontalPickerRef, type PickerValues } from 'expo-horizontal-picker';
169
+ import { useRef, useState } from 'react';
170
+ import { Text, View } from 'react-native';
114
171
 
115
172
  export default function RefExample() {
116
- const pickerRef = useRef<HorizontalPickerRef | null>(null);
173
+ const firstPickerRef = useRef<HorizontalPickerRef | null>(null);
174
+ const secondPickerRef = useRef<HorizontalPickerRef | null>(null);
175
+ const [selected, setSelected] = useState<PickerValues>({
176
+ index: 0,
177
+ value: items[0].value,
178
+ });
117
179
 
118
180
  return (
119
- <>
120
- <HorizontalPicker ref={pickerRef} items={items} />
121
- <Button title="Jump to start" onPress={() => pickerRef.current?.scrollToOffset({ offset: 0, animated: true })} />
122
- </>
181
+ <View>
182
+ <Text>Selected: {selected.value}</Text>
183
+
184
+ <HorizontalPicker
185
+ ref={firstPickerRef}
186
+ items={items}
187
+ containerHeight={53}
188
+ onChange={(value, index) => {
189
+ setSelected({ index, value });
190
+ secondPickerRef.current?.scrollToIndex({ index, animated: true });
191
+ }}
192
+ />
193
+
194
+ <HorizontalPicker
195
+ ref={secondPickerRef}
196
+ items={items}
197
+ containerHeight={53}
198
+ onChange={(value, index) => {
199
+ setSelected({ index, value });
200
+ firstPickerRef.current?.scrollToIndex({ index, animated: true });
201
+ }}
202
+ />
203
+ </View>
123
204
  );
124
205
  }
125
206
  ```
126
207
 
127
208
  ## 📱 Example App
128
209
 
129
- 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`
130
214
 
131
215
  ```bash
132
216
  cd example
@@ -143,6 +227,7 @@ Customize the focused and unfocused item styles with GPU-accelerated properties:
143
227
  ```ts
144
228
  <HorizontalPicker
145
229
  items={items}
230
+ containerHeight={53}
146
231
  focusedTransformStyle={[{ scale: 1.2 }]}
147
232
  unfocusedTransformStyle={[{ scale: 0.9 }]}
148
233
  focusedOpacityStyle={1}
@@ -159,6 +244,7 @@ Customize the focused and unfocused item styles with GPU-accelerated properties:
159
244
  | `items` | `PickerOption[]` | – | Array of options to display. Each option is an object with `label` and `value`. |
160
245
  | `initialScrollIndex` | `number` | `0` | Index of the item initially selected. |
161
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. |
162
248
  | `onChange` | `(value: string \| number, index: number) => void` | – | Callback triggered when the selected item changes. |
163
249
  | `focusedTransformStyle` | `ViewStyle['transform']` | `[{ scale: 1.15 }]` | Transform style applied to the focused item (GPU-accelerated). |
164
250
  | `unfocusedTransformStyle` | `ViewStyle['transform']` | `[{ scale: 1 }]` | Transform style applied to unfocused items (GPU-accelerated). |
@@ -178,7 +264,7 @@ The component extends `FlatListPropsWithLayout`, so you can also pass any valid
178
264
  - `showsHorizontalScrollIndicator` (default: `false`)
179
265
  - `initialNumToRender` (default: `15`)
180
266
  - `maxToRenderPerBatch` (default: `15`)
181
- - `removeClippedSubviews` (default: `true`)
267
+ - `removeClippedSubviews` (default: `Platform.OS !== 'android'`)
182
268
  - `ref` to call `scrollToEnd`, `scrollToIndex`, `scrollToItem`, and `scrollToOffset`
183
269
 
184
270
  ## ⚡ Performance Notes
package/build/index.d.ts CHANGED
@@ -7,6 +7,10 @@ export interface PickerOption {
7
7
  label: string;
8
8
  value: string | number;
9
9
  }
10
+ export interface PickerValues {
11
+ index: number;
12
+ value: PickerOption['value'];
13
+ }
10
14
  export interface HorizontalPickerRef {
11
15
  scrollToEnd: (params?: {
12
16
  animated?: boolean | null;
@@ -31,6 +35,7 @@ export interface HorizontalPickerRef {
31
35
  export interface HorizontalPickerProps extends FlatListProps {
32
36
  ref?: Ref<HorizontalPickerRef | null>;
33
37
  items: PickerOption[];
38
+ containerHeight: number;
34
39
  initialScrollIndex?: number;
35
40
  visibleItemCount?: number;
36
41
  onChange?: (value: PickerOption['value'], index: number) => void;
@@ -41,6 +46,6 @@ export interface HorizontalPickerProps extends FlatListProps {
41
46
  pickerItemStyle?: ViewStyle;
42
47
  pickerItemTextStyle?: TextStyle;
43
48
  }
44
- 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;
45
50
  export {};
46
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,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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-horizontal-picker",
3
- "version": "0.2.0",
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",
@@ -10,12 +10,11 @@
10
10
  "changeset:publish": "yarn build && changeset publish",
11
11
  "changeset:version": "changeset version && yarn --lockfile-only",
12
12
  "clean": "expo-module clean",
13
- "lint": "expo-module lint",
14
- "test": "expo-module test",
15
13
  "prepublishOnly": "expo-module prepublishOnly",
16
14
  "expo-module": "expo-module",
17
15
  "open:ios": "xed example/ios",
18
- "open:android": "open -a \"Android Studio\" example/android"
16
+ "open:android": "open -a \"Android Studio\" example/android",
17
+ "lint": "biome lint"
19
18
  },
20
19
  "keywords": [
21
20
  "react-native",
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,
@@ -37,6 +38,11 @@ export interface PickerOption {
37
38
  value: string | number;
38
39
  }
39
40
 
41
+ export interface PickerValues {
42
+ index: number;
43
+ value: PickerOption['value'];
44
+ }
45
+
40
46
  export interface HorizontalPickerRef {
41
47
  scrollToEnd: (params?: { animated?: boolean | null }) => void;
42
48
  scrollToIndex: (params: {
@@ -57,6 +63,7 @@ export interface HorizontalPickerRef {
57
63
  export interface HorizontalPickerProps extends FlatListProps {
58
64
  ref?: Ref<HorizontalPickerRef | null>;
59
65
  items: PickerOption[];
66
+ containerHeight: number;
60
67
  initialScrollIndex?: number;
61
68
  visibleItemCount?: number;
62
69
  onChange?: (value: PickerOption['value'], index: number) => void;
@@ -71,6 +78,7 @@ export interface HorizontalPickerProps extends FlatListProps {
71
78
  export function HorizontalPicker({
72
79
  ref,
73
80
  items,
81
+ containerHeight,
74
82
  initialScrollIndex = 0,
75
83
  visibleItemCount = 7,
76
84
  onChange,
@@ -81,7 +89,7 @@ export function HorizontalPicker({
81
89
  showsHorizontalScrollIndicator = false,
82
90
  initialNumToRender = 15,
83
91
  maxToRenderPerBatch = 15,
84
- removeClippedSubviews = true,
92
+ removeClippedSubviews = Platform.OS !== 'android',
85
93
  focusedTransformStyle = [{ scale: 1.15 }],
86
94
  unfocusedTransformStyle = [{ scale: 1 }],
87
95
  focusedOpacityStyle = 1,
@@ -97,15 +105,32 @@ export function HorizontalPicker({
97
105
  const currentIndex = useSharedValue<number>(initialScrollIndex);
98
106
 
99
107
  const { itemWidth, paddingSide } = useMemo(() => {
108
+ if (width <= 0) {
109
+ return {
110
+ itemWidth: 0,
111
+ paddingSide: 0,
112
+ };
113
+ }
114
+
100
115
  const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);
101
116
  const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);
102
117
  return {
103
118
  itemWidth: itemWidth,
104
119
  paddingSide: paddingSide,
105
120
  };
106
- }, [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]);
107
129
 
108
130
  const snapOffsets = useMemo(() => {
131
+ if (itemWidth <= 0) {
132
+ return [];
133
+ }
109
134
  return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));
110
135
  }, [items, itemWidth]);
111
136
 
@@ -117,6 +142,9 @@ export function HorizontalPicker({
117
142
  };
118
143
 
119
144
  const scrollToIndex = (index: number) => {
145
+ if (itemWidth <= 0) {
146
+ return;
147
+ }
120
148
  listRef.current?.scrollToOffset({
121
149
  offset: index * itemWidth,
122
150
  animated: true,
@@ -125,11 +153,17 @@ export function HorizontalPicker({
125
153
 
126
154
  const onScroll = useAnimatedScrollHandler({
127
155
  onScroll: (e) => {
156
+ if (itemWidth <= 0) {
157
+ return;
158
+ }
128
159
  const newIndex = Math.round(e.contentOffset.x / itemWidth);
129
160
  const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
130
161
  currentIndex.value = safeIndex;
131
162
  },
132
163
  onMomentumEnd: (e) => {
164
+ if (itemWidth <= 0) {
165
+ return;
166
+ }
133
167
  const newIndex = Math.round(e.contentOffset.x / itemWidth);
134
168
  const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
135
169
  currentIndex.value = safeIndex;
@@ -149,50 +183,61 @@ export function HorizontalPicker({
149
183
  );
150
184
 
151
185
  return (
152
- <Animated.FlatList
153
- {...props}
154
- ref={listRef}
155
- horizontal={true}
156
- data={items}
157
- keyExtractor={keyExtractor}
158
- renderItem={({ item, index }) => (
159
- <PickerItem
160
- label={item.label}
161
- index={index}
162
- itemWidth={itemWidth}
163
- currentIndex={currentIndex}
164
- onPress={() => scrollToIndex(index)}
165
- focusedTransformStyle={focusedTransformStyle}
166
- unfocusedTransformStyle={unfocusedTransformStyle}
167
- focusedOpacityStyle={focusedOpacityStyle}
168
- unfocusedOpacityStyle={unfocusedOpacityStyle}
169
- pickerItemStyle={pickerItemStyle}
170
- pickerItemTextStyle={pickerItemTextStyle}
171
- />
172
- )}
173
- onScroll={onScroll}
174
- scrollEventThrottle={scrollEventThrottle}
175
- decelerationRate={decelerationRate}
186
+ <View
187
+ style={[styles.container, { minHeight: containerMinHeight }]}
176
188
  onLayout={(e) => {
177
- if (typeof onLayout === 'function') {
178
- onLayout(e);
189
+ const nextWidth = e.nativeEvent.layout.width;
190
+ if (nextWidth <= 0) {
191
+ return;
179
192
  }
180
- setWidth(e.nativeEvent.layout.width);
193
+ if (nextWidth === width) {
194
+ return;
195
+ }
196
+ setWidth(nextWidth);
181
197
  }}
182
- showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}
183
- snapToOffsets={snapOffsets}
184
- contentContainerStyle={{ paddingHorizontal: paddingSide }}
185
- getItemLayout={(_, index) => ({
186
- length: itemWidth,
187
- offset: itemWidth * index,
188
- index,
189
- })}
190
- initialScrollIndex={initialScrollIndex}
191
- initialNumToRender={initialNumToRender}
192
- maxToRenderPerBatch={maxToRenderPerBatch}
193
- removeClippedSubviews={removeClippedSubviews}
194
- style={[styles.container, style]}
195
- />
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>
196
241
  );
197
242
  }
198
243
  interface PickerItemProps