expo-horizontal-picker 0.1.8 → 0.2.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 +53 -4
- package/build/index.d.ts +28 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +12 -14
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.tsx +52 -17
package/README.md
CHANGED
|
@@ -30,14 +30,22 @@ This package requires [`react-native-reanimated`](https://docs.expo.dev/versions
|
|
|
30
30
|
npm install expo-horizontal-picker react-native-reanimated
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`ref` support uses React 19's ref-as-prop model, so install this package in a React 19 app.
|
|
34
|
+
|
|
33
35
|
Make sure to follow the additional setup instructions for Reanimated in the [official docs](https://docs.expo.dev/versions/latest/sdk/reanimated/#installation).
|
|
34
36
|
|
|
35
37
|
## 🎬 Demo
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
36
41
|
```ts
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
42
|
+
import { useRef } from 'react';
|
|
43
|
+
import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
|
|
44
|
+
import { Button, View } from 'react-native';
|
|
39
45
|
|
|
40
46
|
export default function App() {
|
|
47
|
+
const pickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
48
|
+
|
|
41
49
|
return (
|
|
42
50
|
<View style={styles.container}>
|
|
43
51
|
<View>
|
|
@@ -46,7 +54,7 @@ export default function App() {
|
|
|
46
54
|
label: `${i + 1}`,
|
|
47
55
|
value: i + 1,
|
|
48
56
|
}))}
|
|
49
|
-
initialScrollIndex={
|
|
57
|
+
initialScrollIndex={499}
|
|
50
58
|
visibleItemCount={7}
|
|
51
59
|
/>
|
|
52
60
|
|
|
@@ -64,10 +72,16 @@ export default function App() {
|
|
|
64
72
|
label: `${i + 1}h`,
|
|
65
73
|
value: i + 1,
|
|
66
74
|
}))}
|
|
75
|
+
ref={pickerRef}
|
|
67
76
|
initialScrollIndex={11}
|
|
68
77
|
visibleItemCount={3}
|
|
69
78
|
/>
|
|
70
79
|
|
|
80
|
+
<Button
|
|
81
|
+
title="Jump to 24h"
|
|
82
|
+
onPress={() => pickerRef.current?.scrollToEnd({ animated: true })}
|
|
83
|
+
/>
|
|
84
|
+
|
|
71
85
|
<HorizontalPicker
|
|
72
86
|
items={Array.from({ length: 5 }, (_, i) => ({
|
|
73
87
|
label: `${(i + 1) * 10000}`,
|
|
@@ -89,6 +103,39 @@ const styles = {
|
|
|
89
103
|
};
|
|
90
104
|
```
|
|
91
105
|
|
|
106
|
+
## Ref Usage
|
|
107
|
+
|
|
108
|
+
Pass a ref when you need the picker scroll methods: `scrollToEnd`, `scrollToIndex`, `scrollToItem`, or `scrollToOffset`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { useRef } from 'react';
|
|
112
|
+
import { Button } from 'react-native';
|
|
113
|
+
import { HorizontalPicker, type HorizontalPickerRef } from 'expo-horizontal-picker';
|
|
114
|
+
|
|
115
|
+
export default function RefExample() {
|
|
116
|
+
const pickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
<HorizontalPicker ref={pickerRef} items={items} />
|
|
121
|
+
<Button title="Jump to start" onPress={() => pickerRef.current?.scrollToOffset({ offset: 0, animated: true })} />
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 📱 Example App
|
|
128
|
+
|
|
129
|
+
A runnable Expo example app is included in [`example`](./example). It mirrors the README demo and includes ref-driven scroll controls.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
cd example
|
|
133
|
+
yarn
|
|
134
|
+
yarn start
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The example app resolves `expo-horizontal-picker` to this repository's local `src` directory through Metro, so you can iterate on the library and see changes immediately in the app.
|
|
138
|
+
|
|
92
139
|
## 🎨 Customization Example
|
|
93
140
|
|
|
94
141
|
Customize the focused and unfocused item styles with GPU-accelerated properties:
|
|
@@ -118,6 +165,7 @@ Customize the focused and unfocused item styles with GPU-accelerated properties:
|
|
|
118
165
|
| `focusedOpacityStyle` | `number` | `1` | Opacity value for the focused item (GPU-accelerated). |
|
|
119
166
|
| `unfocusedOpacityStyle` | `number` | `0.2` | Opacity value for unfocused items (GPU-accelerated). |
|
|
120
167
|
| `pickerItemStyle` | `ViewStyle` | – | Style applied to each picker item container. |
|
|
168
|
+
| `pickerItemTextStyle` | `TextStyle` | – | Style applied to the text inside each picker item. |
|
|
121
169
|
| `style` | `ViewStyle` | – | Style applied to the scroll container. |
|
|
122
170
|
|
|
123
171
|
### Additional FlatList Props
|
|
@@ -131,7 +179,8 @@ The component extends `FlatListPropsWithLayout`, so you can also pass any valid
|
|
|
131
179
|
- `initialNumToRender` (default: `15`)
|
|
132
180
|
- `maxToRenderPerBatch` (default: `15`)
|
|
133
181
|
- `removeClippedSubviews` (default: `true`)
|
|
182
|
+
- `ref` to call `scrollToEnd`, `scrollToIndex`, `scrollToItem`, and `scrollToOffset`
|
|
134
183
|
|
|
135
184
|
## ⚡ Performance Notes
|
|
136
185
|
|
|
137
|
-
The `focusedTransformStyle` and `unfocusedTransformStyle` props only accept GPU-accelerated transform properties (e.g., `scale`, `translateX`, `translateY`, `rotate`) for optimal performance. These properties are processed directly on the GPU without triggering layout recalculations.
|
|
186
|
+
The `focusedTransformStyle` and `unfocusedTransformStyle` props only accept GPU-accelerated transform properties (e.g., `scale`, `translateX`, `translateY`, `rotate`) for optimal performance. These properties are processed directly on the GPU without triggering layout recalculations.
|
package/build/index.d.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Ref } from 'react';
|
|
2
|
+
import { type TextStyle, type ViewStyle } from 'react-native';
|
|
2
3
|
import { type FlatListPropsWithLayout } from 'react-native-reanimated';
|
|
3
|
-
interface PickerOption {
|
|
4
|
+
interface FlatListProps extends Omit<FlatListPropsWithLayout<PickerOption>, 'ref' | 'horizontal' | 'data' | 'renderItem' | 'initialScrollIndex' | 'onScroll' | 'snapToOffsets' | 'contentContainerStyle' | 'getItemLayout'> {
|
|
5
|
+
}
|
|
6
|
+
export interface PickerOption {
|
|
4
7
|
label: string;
|
|
5
8
|
value: string | number;
|
|
6
9
|
}
|
|
7
|
-
interface
|
|
10
|
+
export interface HorizontalPickerRef {
|
|
11
|
+
scrollToEnd: (params?: {
|
|
12
|
+
animated?: boolean | null;
|
|
13
|
+
}) => void;
|
|
14
|
+
scrollToIndex: (params: {
|
|
15
|
+
animated?: boolean | null;
|
|
16
|
+
index: number;
|
|
17
|
+
viewOffset?: number;
|
|
18
|
+
viewPosition?: number;
|
|
19
|
+
}) => void;
|
|
20
|
+
scrollToItem: (params: {
|
|
21
|
+
animated?: boolean | null;
|
|
22
|
+
item: PickerOption;
|
|
23
|
+
viewOffset?: number;
|
|
24
|
+
viewPosition?: number;
|
|
25
|
+
}) => void;
|
|
26
|
+
scrollToOffset: (params: {
|
|
27
|
+
animated?: boolean | null;
|
|
28
|
+
offset: number;
|
|
29
|
+
}) => void;
|
|
8
30
|
}
|
|
9
31
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
32
|
+
ref?: Ref<HorizontalPickerRef | null>;
|
|
10
33
|
items: PickerOption[];
|
|
11
34
|
initialScrollIndex?: number;
|
|
12
35
|
visibleItemCount?: number;
|
|
@@ -16,7 +39,8 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
16
39
|
focusedOpacityStyle?: number;
|
|
17
40
|
unfocusedOpacityStyle?: number;
|
|
18
41
|
pickerItemStyle?: ViewStyle;
|
|
42
|
+
pickerItemTextStyle?: TextStyle;
|
|
19
43
|
}
|
|
20
|
-
export declare function HorizontalPicker({ items, initialScrollIndex, visibleItemCount, onChange, keyExtractor, scrollEventThrottle, decelerationRate, onLayout, showsHorizontalScrollIndicator, initialNumToRender, maxToRenderPerBatch, removeClippedSubviews, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, style, ...props }: HorizontalPickerProps): import("react").JSX.Element;
|
|
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;
|
|
21
45
|
export {};
|
|
22
46
|
//# 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":"
|
|
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"}
|
package/build/index.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { PixelRatio, Pressable, StyleSheet, View } from 'react-native';
|
|
1
|
+
import { useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { PixelRatio, Pressable, StyleSheet, View, } from 'react-native';
|
|
3
3
|
import Animated, { runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useDerivedValue, useSharedValue, } from 'react-native-reanimated';
|
|
4
|
-
export function HorizontalPicker({ 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, style, ...props }) {
|
|
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 }) {
|
|
5
5
|
const listRef = useRef(null);
|
|
6
6
|
const [width, setWidth] = useState(0);
|
|
7
|
-
const scrollX = useSharedValue(0);
|
|
8
7
|
const currentIndex = useSharedValue(initialScrollIndex);
|
|
9
8
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
10
9
|
const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);
|
|
@@ -31,7 +30,6 @@ export function HorizontalPicker({ items, initialScrollIndex = 0, visibleItemCou
|
|
|
31
30
|
};
|
|
32
31
|
const onScroll = useAnimatedScrollHandler({
|
|
33
32
|
onScroll: (e) => {
|
|
34
|
-
scrollX.value = e.contentOffset.x;
|
|
35
33
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
36
34
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
37
35
|
currentIndex.value = safeIndex;
|
|
@@ -43,7 +41,13 @@ export function HorizontalPicker({ items, initialScrollIndex = 0, visibleItemCou
|
|
|
43
41
|
runOnJS(handleOnChange)(safeIndex);
|
|
44
42
|
},
|
|
45
43
|
});
|
|
46
|
-
|
|
44
|
+
useImperativeHandle(ref, () => ({
|
|
45
|
+
scrollToEnd: (params) => listRef.current?.scrollToEnd(params),
|
|
46
|
+
scrollToIndex: (params) => listRef.current?.scrollToIndex(params),
|
|
47
|
+
scrollToItem: (params) => listRef.current?.scrollToItem(params),
|
|
48
|
+
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
|
|
49
|
+
}), []);
|
|
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) => {
|
|
47
51
|
if (typeof onLayout === 'function') {
|
|
48
52
|
onLayout(e);
|
|
49
53
|
}
|
|
@@ -54,7 +58,7 @@ export function HorizontalPicker({ items, initialScrollIndex = 0, visibleItemCou
|
|
|
54
58
|
index,
|
|
55
59
|
})} initialScrollIndex={initialScrollIndex} initialNumToRender={initialNumToRender} maxToRenderPerBatch={maxToRenderPerBatch} removeClippedSubviews={removeClippedSubviews} style={[styles.container, style]}/>);
|
|
56
60
|
}
|
|
57
|
-
function PickerItem({ label, index, itemWidth, currentIndex, onPress, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, }) {
|
|
61
|
+
function PickerItem({ label, index, itemWidth, currentIndex, onPress, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, }) {
|
|
58
62
|
const isFocused = useDerivedValue(() => currentIndex.value === index);
|
|
59
63
|
const animatedStyle = useAnimatedStyle(() => {
|
|
60
64
|
return {
|
|
@@ -64,7 +68,7 @@ function PickerItem({ label, index, itemWidth, currentIndex, onPress, focusedTra
|
|
|
64
68
|
}, [index]);
|
|
65
69
|
return (<Pressable onPress={onPress}>
|
|
66
70
|
<View style={[styles.itemContainer, { width: itemWidth }, pickerItemStyle]}>
|
|
67
|
-
<Animated.Text style={[styles.itemText, animatedStyle]}>{label}</Animated.Text>
|
|
71
|
+
<Animated.Text style={[styles.itemText, animatedStyle, pickerItemTextStyle]}>{label}</Animated.Text>
|
|
68
72
|
</View>
|
|
69
73
|
</Pressable>);
|
|
70
74
|
}
|
|
@@ -75,16 +79,10 @@ const styles = StyleSheet.create({
|
|
|
75
79
|
itemContainer: {
|
|
76
80
|
justifyContent: 'center',
|
|
77
81
|
alignItems: 'center',
|
|
78
|
-
height: 60,
|
|
79
82
|
},
|
|
80
83
|
itemText: {
|
|
81
84
|
fontSize: 13,
|
|
82
85
|
fontWeight: '700',
|
|
83
|
-
color: '#C9CED9',
|
|
84
|
-
},
|
|
85
|
-
itemTextSelected: {
|
|
86
|
-
fontSize: 15,
|
|
87
|
-
fontWeight: '800',
|
|
88
86
|
color: '#000000',
|
|
89
87
|
},
|
|
90
88
|
});
|
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 { useMemo, useRef, useState } from 'react';\nimport { PixelRatio, Pressable, StyleSheet, View, type ViewStyle } 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 PickerOption {\n label: string;\n value: string | number;\n}\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 HorizontalPickerProps extends FlatListProps {\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}\n\nexport function HorizontalPicker({\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 style,\n ...props\n}: HorizontalPickerProps) {\n const listRef = useRef<Animated.FlatList<PickerOption>>(null);\n const [width, setWidth] = useState<number>(0);\n\n const scrollX = useSharedValue<number>(0);\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 scrollX.value = e.contentOffset.x;\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 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 />\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 > {\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}: 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]}>{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 height: 60,\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#C9CED9',\n },\n itemTextSelected: {\n fontSize: 15,\n fontWeight: '800',\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,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;AAoDjC,MAAM,UAAU,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,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,IAAI,EAC5B,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,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,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAE9B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,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,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,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,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,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,CAAC,CAAC,EAAE,EAAE;YACd,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;gBACnC,QAAQ,CAAC,CAAC,CAAC,CAAC;YACd,CAAC;YACD,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC,CAAC,CACF,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;YAC5B,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,SAAS,GAAG,KAAK;YACzB,KAAK;SACN,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,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,EACjC,CACH,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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-horizontal-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"expo": "*",
|
|
57
|
-
"react": "
|
|
57
|
+
"react": "^19.0.0",
|
|
58
58
|
"react-native": "*",
|
|
59
59
|
"react-native-reanimated": ">=2.10.0"
|
|
60
60
|
},
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { useMemo, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { type Ref, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type FlatList as NativeFlatList,
|
|
4
|
+
PixelRatio,
|
|
5
|
+
Pressable,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
View,
|
|
9
|
+
type ViewStyle,
|
|
10
|
+
} from 'react-native';
|
|
3
11
|
import Animated, {
|
|
4
12
|
type FlatListPropsWithLayout,
|
|
5
13
|
type SharedValue,
|
|
@@ -10,11 +18,6 @@ import Animated, {
|
|
|
10
18
|
useSharedValue,
|
|
11
19
|
} from 'react-native-reanimated';
|
|
12
20
|
|
|
13
|
-
interface PickerOption {
|
|
14
|
-
label: string;
|
|
15
|
-
value: string | number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
21
|
interface FlatListProps
|
|
19
22
|
extends Omit<
|
|
20
23
|
FlatListPropsWithLayout<PickerOption>,
|
|
@@ -29,7 +32,30 @@ interface FlatListProps
|
|
|
29
32
|
| 'getItemLayout'
|
|
30
33
|
> {}
|
|
31
34
|
|
|
35
|
+
export interface PickerOption {
|
|
36
|
+
label: string;
|
|
37
|
+
value: string | number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface HorizontalPickerRef {
|
|
41
|
+
scrollToEnd: (params?: { animated?: boolean | null }) => void;
|
|
42
|
+
scrollToIndex: (params: {
|
|
43
|
+
animated?: boolean | null;
|
|
44
|
+
index: number;
|
|
45
|
+
viewOffset?: number;
|
|
46
|
+
viewPosition?: number;
|
|
47
|
+
}) => void;
|
|
48
|
+
scrollToItem: (params: {
|
|
49
|
+
animated?: boolean | null;
|
|
50
|
+
item: PickerOption;
|
|
51
|
+
viewOffset?: number;
|
|
52
|
+
viewPosition?: number;
|
|
53
|
+
}) => void;
|
|
54
|
+
scrollToOffset: (params: { animated?: boolean | null; offset: number }) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
58
|
+
ref?: Ref<HorizontalPickerRef | null>;
|
|
33
59
|
items: PickerOption[];
|
|
34
60
|
initialScrollIndex?: number;
|
|
35
61
|
visibleItemCount?: number;
|
|
@@ -39,9 +65,11 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
39
65
|
focusedOpacityStyle?: number;
|
|
40
66
|
unfocusedOpacityStyle?: number;
|
|
41
67
|
pickerItemStyle?: ViewStyle;
|
|
68
|
+
pickerItemTextStyle?: TextStyle;
|
|
42
69
|
}
|
|
43
70
|
|
|
44
71
|
export function HorizontalPicker({
|
|
72
|
+
ref,
|
|
45
73
|
items,
|
|
46
74
|
initialScrollIndex = 0,
|
|
47
75
|
visibleItemCount = 7,
|
|
@@ -59,13 +87,13 @@ export function HorizontalPicker({
|
|
|
59
87
|
focusedOpacityStyle = 1,
|
|
60
88
|
unfocusedOpacityStyle = 0.2,
|
|
61
89
|
pickerItemStyle,
|
|
90
|
+
pickerItemTextStyle,
|
|
62
91
|
style,
|
|
63
92
|
...props
|
|
64
93
|
}: HorizontalPickerProps) {
|
|
65
|
-
const listRef = useRef<
|
|
94
|
+
const listRef = useRef<NativeFlatList<PickerOption> | null>(null);
|
|
66
95
|
const [width, setWidth] = useState<number>(0);
|
|
67
96
|
|
|
68
|
-
const scrollX = useSharedValue<number>(0);
|
|
69
97
|
const currentIndex = useSharedValue<number>(initialScrollIndex);
|
|
70
98
|
|
|
71
99
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
@@ -97,7 +125,6 @@ export function HorizontalPicker({
|
|
|
97
125
|
|
|
98
126
|
const onScroll = useAnimatedScrollHandler({
|
|
99
127
|
onScroll: (e) => {
|
|
100
|
-
scrollX.value = e.contentOffset.x;
|
|
101
128
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
102
129
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
103
130
|
currentIndex.value = safeIndex;
|
|
@@ -110,6 +137,17 @@ export function HorizontalPicker({
|
|
|
110
137
|
},
|
|
111
138
|
});
|
|
112
139
|
|
|
140
|
+
useImperativeHandle(
|
|
141
|
+
ref,
|
|
142
|
+
() => ({
|
|
143
|
+
scrollToEnd: (params) => listRef.current?.scrollToEnd(params),
|
|
144
|
+
scrollToIndex: (params) => listRef.current?.scrollToIndex(params),
|
|
145
|
+
scrollToItem: (params) => listRef.current?.scrollToItem(params),
|
|
146
|
+
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
|
|
147
|
+
}),
|
|
148
|
+
[],
|
|
149
|
+
);
|
|
150
|
+
|
|
113
151
|
return (
|
|
114
152
|
<Animated.FlatList
|
|
115
153
|
{...props}
|
|
@@ -129,6 +167,7 @@ export function HorizontalPicker({
|
|
|
129
167
|
focusedOpacityStyle={focusedOpacityStyle}
|
|
130
168
|
unfocusedOpacityStyle={unfocusedOpacityStyle}
|
|
131
169
|
pickerItemStyle={pickerItemStyle}
|
|
170
|
+
pickerItemTextStyle={pickerItemTextStyle}
|
|
132
171
|
/>
|
|
133
172
|
)}
|
|
134
173
|
onScroll={onScroll}
|
|
@@ -164,6 +203,7 @@ interface PickerItemProps
|
|
|
164
203
|
| 'focusedOpacityStyle'
|
|
165
204
|
| 'unfocusedOpacityStyle'
|
|
166
205
|
| 'pickerItemStyle'
|
|
206
|
+
| 'pickerItemTextStyle'
|
|
167
207
|
> {
|
|
168
208
|
label: string;
|
|
169
209
|
index: number;
|
|
@@ -183,6 +223,7 @@ function PickerItem({
|
|
|
183
223
|
focusedOpacityStyle,
|
|
184
224
|
unfocusedOpacityStyle,
|
|
185
225
|
pickerItemStyle,
|
|
226
|
+
pickerItemTextStyle,
|
|
186
227
|
}: PickerItemProps) {
|
|
187
228
|
const isFocused = useDerivedValue(() => currentIndex.value === index);
|
|
188
229
|
|
|
@@ -196,7 +237,7 @@ function PickerItem({
|
|
|
196
237
|
return (
|
|
197
238
|
<Pressable onPress={onPress}>
|
|
198
239
|
<View style={[styles.itemContainer, { width: itemWidth }, pickerItemStyle]}>
|
|
199
|
-
<Animated.Text style={[styles.itemText, animatedStyle]}>{label}</Animated.Text>
|
|
240
|
+
<Animated.Text style={[styles.itemText, animatedStyle, pickerItemTextStyle]}>{label}</Animated.Text>
|
|
200
241
|
</View>
|
|
201
242
|
</Pressable>
|
|
202
243
|
);
|
|
@@ -209,16 +250,10 @@ const styles = StyleSheet.create({
|
|
|
209
250
|
itemContainer: {
|
|
210
251
|
justifyContent: 'center',
|
|
211
252
|
alignItems: 'center',
|
|
212
|
-
height: 60,
|
|
213
253
|
},
|
|
214
254
|
itemText: {
|
|
215
255
|
fontSize: 13,
|
|
216
256
|
fontWeight: '700',
|
|
217
|
-
color: '#C9CED9',
|
|
218
|
-
},
|
|
219
|
-
itemTextSelected: {
|
|
220
|
-
fontSize: 15,
|
|
221
|
-
fontWeight: '800',
|
|
222
257
|
color: '#000000',
|
|
223
258
|
},
|
|
224
259
|
});
|