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 +130 -44
- package/build/index.d.ts +6 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +42 -12
- package/build/index.js.map +1 -1
- package/package.json +3 -4
- package/src/index.tsx +88 -43
package/README.md
CHANGED
|
@@ -39,94 +39,178 @@ Make sure to follow the additional setup instructions for Reanimated in the [off
|
|
|
39
39
|

|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
import { useRef } from 'react';
|
|
43
42
|
import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
|
|
44
|
-
import {
|
|
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
|
|
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
|
-
<
|
|
51
|
-
<
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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={
|
|
63
|
-
|
|
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={
|
|
72
|
-
|
|
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={
|
|
87
|
-
|
|
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
|
-
</
|
|
94
|
-
</
|
|
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: '#
|
|
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 {
|
|
112
|
-
import {
|
|
113
|
-
import {
|
|
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
|
|
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
|
-
<
|
|
121
|
-
|
|
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
|
|
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: `
|
|
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
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAkD,MAAM,OAAO,CAAC;AACjF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAkD,MAAM,OAAO,CAAC;AACjF,OAAO,EAML,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAiB,EACf,KAAK,uBAAuB,EAO7B,MAAM,yBAAyB,CAAC;AAEjC,UAAU,aACR,SAAQ,IAAI,CACV,uBAAuB,CAAC,YAAY,CAAC,EACnC,KAAK,GACL,YAAY,GACZ,MAAM,GACN,YAAY,GACZ,oBAAoB,GACpB,UAAU,GACV,eAAe,GACf,uBAAuB,GACvB,eAAe,CAClB;CAAG;AAEN,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9D,aAAa,EAAE,CAAC,MAAM,EAAE;QACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,YAAY,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,IAAI,EAAE,YAAY,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,cAAc,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjF;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,GAAG,CAAC,EAAE,GAAG,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACtC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,qBAAqB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAC/C,uBAAuB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,mBAAmB,CAAC,EAAE,SAAS,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,eAAe,EACf,kBAAsB,EACtB,gBAAoB,EACpB,QAAQ,EACR,YAAwD,EACxD,mBAAwB,EACxB,gBAAyB,EACzB,QAAQ,EACR,8BAAsC,EACtC,kBAAuB,EACvB,mBAAwB,EACxB,qBAAiD,EACjD,qBAAyC,EACzC,uBAAwC,EACxC,mBAAuB,EACvB,qBAA2B,EAC3B,eAAe,EACf,mBAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,qBAAqB,+BA6IvB"}
|
package/build/index.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { PixelRatio, Pressable, StyleSheet, View, } from 'react-native';
|
|
2
|
+
import { PixelRatio, Platform, Pressable, StyleSheet, View, } from 'react-native';
|
|
3
3
|
import Animated, { runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
|
|
4
|
-
export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleItemCount = 7, onChange, keyExtractor = (item, index) => `${item.value}-${index}`, scrollEventThrottle = 16, decelerationRate = 'fast', onLayout, showsHorizontalScrollIndicator = false, initialNumToRender = 15, maxToRenderPerBatch = 15, removeClippedSubviews =
|
|
4
|
+
export function HorizontalPicker({ ref, items, containerHeight, initialScrollIndex = 0, visibleItemCount = 7, onChange, keyExtractor = (item, index) => `${item.value}-${index}`, scrollEventThrottle = 16, decelerationRate = 'fast', onLayout, showsHorizontalScrollIndicator = false, initialNumToRender = 15, maxToRenderPerBatch = 15, removeClippedSubviews = Platform.OS !== 'android', focusedTransformStyle = [{ scale: 1.15 }], unfocusedTransformStyle = [{ scale: 1 }], focusedOpacityStyle = 1, unfocusedOpacityStyle = 0.2, pickerItemStyle, pickerItemTextStyle, style, ...props }) {
|
|
5
5
|
const listRef = useRef(null);
|
|
6
6
|
const [width, setWidth] = useState(0);
|
|
7
7
|
const currentIndex = useSharedValue(initialScrollIndex);
|
|
8
8
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
9
|
+
if (width <= 0) {
|
|
10
|
+
return {
|
|
11
|
+
itemWidth: 0,
|
|
12
|
+
paddingSide: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
9
15
|
const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);
|
|
10
16
|
const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);
|
|
11
17
|
return {
|
|
12
18
|
itemWidth: itemWidth,
|
|
13
19
|
paddingSide: paddingSide,
|
|
14
20
|
};
|
|
15
|
-
}, [
|
|
21
|
+
}, [visibleItemCount, width]);
|
|
22
|
+
const containerMinHeight = useMemo(() => {
|
|
23
|
+
if (!Number.isFinite(containerHeight)) {
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(containerHeight, 1);
|
|
27
|
+
}, [containerHeight]);
|
|
16
28
|
const snapOffsets = useMemo(() => {
|
|
29
|
+
if (itemWidth <= 0) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
17
32
|
return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));
|
|
18
33
|
}, [items, itemWidth]);
|
|
19
34
|
const handleOnChange = (index) => {
|
|
@@ -23,6 +38,9 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
23
38
|
}
|
|
24
39
|
};
|
|
25
40
|
const scrollToIndex = (index) => {
|
|
41
|
+
if (itemWidth <= 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
26
44
|
listRef.current?.scrollToOffset({
|
|
27
45
|
offset: index * itemWidth,
|
|
28
46
|
animated: true,
|
|
@@ -30,11 +48,17 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
30
48
|
};
|
|
31
49
|
const onScroll = useAnimatedScrollHandler({
|
|
32
50
|
onScroll: (e) => {
|
|
51
|
+
if (itemWidth <= 0) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
33
54
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
34
55
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
35
56
|
currentIndex.value = safeIndex;
|
|
36
57
|
},
|
|
37
58
|
onMomentumEnd: (e) => {
|
|
59
|
+
if (itemWidth <= 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
38
62
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
39
63
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
40
64
|
currentIndex.value = safeIndex;
|
|
@@ -47,16 +71,22 @@ export function HorizontalPicker({ ref, items, initialScrollIndex = 0, visibleIt
|
|
|
47
71
|
scrollToItem: (params) => listRef.current?.scrollToItem(params),
|
|
48
72
|
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
|
|
49
73
|
}), []);
|
|
50
|
-
return (<
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
return (<View style={[styles.container, { minHeight: containerMinHeight }]} onLayout={(e) => {
|
|
75
|
+
const nextWidth = e.nativeEvent.layout.width;
|
|
76
|
+
if (nextWidth <= 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (nextWidth === width) {
|
|
80
|
+
return;
|
|
53
81
|
}
|
|
54
|
-
setWidth(
|
|
55
|
-
}}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
setWidth(nextWidth);
|
|
83
|
+
}}>
|
|
84
|
+
{width > 0 && (<Animated.FlatList {...props} ref={listRef} horizontal={true} data={items} keyExtractor={keyExtractor} renderItem={({ item, index }) => (<PickerItem label={item.label} index={index} itemWidth={itemWidth} currentIndex={currentIndex} onPress={() => scrollToIndex(index)} focusedTransformStyle={focusedTransformStyle} unfocusedTransformStyle={unfocusedTransformStyle} focusedOpacityStyle={focusedOpacityStyle} unfocusedOpacityStyle={unfocusedOpacityStyle} pickerItemStyle={pickerItemStyle} pickerItemTextStyle={pickerItemTextStyle}/>)} onScroll={onScroll} scrollEventThrottle={scrollEventThrottle} decelerationRate={decelerationRate} onLayout={onLayout} showsHorizontalScrollIndicator={showsHorizontalScrollIndicator} snapToOffsets={snapOffsets} contentContainerStyle={{ paddingHorizontal: paddingSide }} getItemLayout={(_, index) => ({
|
|
85
|
+
length: itemWidth,
|
|
86
|
+
offset: itemWidth * index,
|
|
87
|
+
index,
|
|
88
|
+
})} initialScrollIndex={initialScrollIndex} initialNumToRender={initialNumToRender} maxToRenderPerBatch={maxToRenderPerBatch} removeClippedSubviews={removeClippedSubviews} style={style}/>)}
|
|
89
|
+
</View>);
|
|
60
90
|
}
|
|
61
91
|
function PickerItem({ label, index, itemWidth, currentIndex, onPress, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, }) {
|
|
62
92
|
const isFocused = useDerivedValue(() => currentIndex.value === index);
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type Ref, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport {\n type FlatList as NativeFlatList,\n PixelRatio,\n Pressable,\n StyleSheet,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type FlatListPropsWithLayout,\n type SharedValue,\n runOnJS,\n useAnimatedScrollHandler,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface FlatListProps\n extends Omit<\n FlatListPropsWithLayout<PickerOption>,\n | 'ref'\n | 'horizontal'\n | 'data'\n | 'renderItem'\n | 'initialScrollIndex'\n | 'onScroll'\n | 'snapToOffsets'\n | 'contentContainerStyle'\n | 'getItemLayout'\n > {}\n\nexport interface PickerOption {\n label: string;\n value: string | number;\n}\n\nexport interface 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.
|
|
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 =
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
153
|
-
{
|
|
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
|
-
|
|
178
|
-
|
|
189
|
+
const nextWidth = e.nativeEvent.layout.width;
|
|
190
|
+
if (nextWidth <= 0) {
|
|
191
|
+
return;
|
|
179
192
|
}
|
|
180
|
-
|
|
193
|
+
if (nextWidth === width) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setWidth(nextWidth);
|
|
181
197
|
}}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|