expo-horizontal-picker 0.1.9 โ 0.2.1
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 +118 -43
- package/build/index.d.ts +29 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9 -6
- package/build/index.js.map +1 -1
- package/package.json +4 -5
- package/src/index.tsx +51 -11
package/README.md
CHANGED
|
@@ -30,65 +30,139 @@ 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 { HorizontalPicker } from 'expo-horizontal-picker';
|
|
38
|
-
import {
|
|
42
|
+
import { HorizontalPicker, type HorizontalPickerRef, type PickerValues } from 'expo-horizontal-picker';
|
|
43
|
+
import { useRef, useState } from 'react';
|
|
44
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
45
|
+
|
|
46
|
+
const numberItems = Array.from({ length: 600 }, (_, i) => ({
|
|
47
|
+
label: `${i + 1}`,
|
|
48
|
+
value: i + 1,
|
|
49
|
+
}));
|
|
39
50
|
|
|
40
51
|
export default function App() {
|
|
52
|
+
const firstPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
53
|
+
const secondPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
54
|
+
const [selected, setSelected] = useState<PickerValues>({
|
|
55
|
+
index: 499,
|
|
56
|
+
value: numberItems[499].value,
|
|
57
|
+
});
|
|
58
|
+
|
|
41
59
|
return (
|
|
42
60
|
<View style={styles.container}>
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
visibleItemCount={3}
|
|
69
|
-
/>
|
|
70
|
-
|
|
71
|
-
<HorizontalPicker
|
|
72
|
-
items={Array.from({ length: 5 }, (_, i) => ({
|
|
73
|
-
label: `${(i + 1) * 10000}`,
|
|
74
|
-
value: (i + 1) * 10000,
|
|
75
|
-
}))}
|
|
76
|
-
initialScrollIndex={2}
|
|
77
|
-
visibleItemCount={1}
|
|
78
|
-
/>
|
|
79
|
-
</View>
|
|
61
|
+
<Text style={styles.title}>Sync ยท 7 visible โ {selected.value}</Text>
|
|
62
|
+
|
|
63
|
+
<HorizontalPicker
|
|
64
|
+
ref={firstPickerRef}
|
|
65
|
+
items={numberItems}
|
|
66
|
+
initialScrollIndex={499}
|
|
67
|
+
visibleItemCount={7}
|
|
68
|
+
onChange={(value, index) => {
|
|
69
|
+
setSelected({ index, value });
|
|
70
|
+
secondPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
71
|
+
}}
|
|
72
|
+
pickerItemStyle={styles.pickerItem}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<HorizontalPicker
|
|
76
|
+
ref={secondPickerRef}
|
|
77
|
+
items={numberItems}
|
|
78
|
+
initialScrollIndex={499}
|
|
79
|
+
visibleItemCount={7}
|
|
80
|
+
onChange={(value, index) => {
|
|
81
|
+
setSelected({ index, value });
|
|
82
|
+
firstPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
83
|
+
}}
|
|
84
|
+
pickerItemStyle={styles.pickerItem}
|
|
85
|
+
/>
|
|
80
86
|
</View>
|
|
81
87
|
);
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
const styles = {
|
|
90
|
+
const styles = StyleSheet.create({
|
|
85
91
|
container: {
|
|
86
92
|
flex: 1,
|
|
87
|
-
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
paddingHorizontal: 20,
|
|
95
|
+
backgroundColor: '#eeeeee',
|
|
96
|
+
},
|
|
97
|
+
title: {
|
|
98
|
+
marginBottom: 12,
|
|
99
|
+
fontSize: 18,
|
|
100
|
+
fontWeight: '700',
|
|
101
|
+
color: '#111111',
|
|
88
102
|
},
|
|
89
|
-
|
|
103
|
+
pickerItem: {
|
|
104
|
+
paddingVertical: 20,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Ref Usage
|
|
110
|
+
|
|
111
|
+
Pass a ref when you need the picker scroll methods: `scrollToEnd`, `scrollToIndex`, `scrollToItem`, or `scrollToOffset`.
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { HorizontalPicker, type HorizontalPickerRef, type PickerValues } from 'expo-horizontal-picker';
|
|
117
|
+
import { useRef, useState } from 'react';
|
|
118
|
+
import { Text, View } from 'react-native';
|
|
119
|
+
|
|
120
|
+
export default function RefExample() {
|
|
121
|
+
const firstPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
122
|
+
const secondPickerRef = useRef<HorizontalPickerRef | null>(null);
|
|
123
|
+
const [selected, setSelected] = useState<PickerValues>({
|
|
124
|
+
index: 0,
|
|
125
|
+
value: items[0].value,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<View>
|
|
130
|
+
<Text>Selected: {selected.value}</Text>
|
|
131
|
+
|
|
132
|
+
<HorizontalPicker
|
|
133
|
+
ref={firstPickerRef}
|
|
134
|
+
items={items}
|
|
135
|
+
onChange={(value, index) => {
|
|
136
|
+
setSelected({ index, value });
|
|
137
|
+
secondPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<HorizontalPicker
|
|
142
|
+
ref={secondPickerRef}
|
|
143
|
+
items={items}
|
|
144
|
+
onChange={(value, index) => {
|
|
145
|
+
setSelected({ index, value });
|
|
146
|
+
firstPickerRef.current?.scrollToIndex({ index, animated: true });
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
90
152
|
```
|
|
91
153
|
|
|
154
|
+
## ๐ฑ Example App
|
|
155
|
+
|
|
156
|
+
A runnable Expo example app is included in [`example`](./example). It mirrors the README demo and includes ref-driven scroll controls.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cd example
|
|
160
|
+
yarn
|
|
161
|
+
yarn start
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
92
166
|
## ๐จ Customization Example
|
|
93
167
|
|
|
94
168
|
Customize the focused and unfocused item styles with GPU-accelerated properties:
|
|
@@ -132,7 +206,8 @@ The component extends `FlatListPropsWithLayout`, so you can also pass any valid
|
|
|
132
206
|
- `initialNumToRender` (default: `15`)
|
|
133
207
|
- `maxToRenderPerBatch` (default: `15`)
|
|
134
208
|
- `removeClippedSubviews` (default: `true`)
|
|
209
|
+
- `ref` to call `scrollToEnd`, `scrollToIndex`, `scrollToItem`, and `scrollToOffset`
|
|
135
210
|
|
|
136
211
|
## โก Performance Notes
|
|
137
212
|
|
|
138
|
-
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.
|
|
213
|
+
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,39 @@
|
|
|
1
|
+
import { type Ref } from 'react';
|
|
1
2
|
import { type TextStyle, type ViewStyle } from 'react-native';
|
|
2
3
|
import { type FlatListPropsWithLayout } from 'react-native-reanimated';
|
|
4
|
+
interface FlatListProps extends Omit<FlatListPropsWithLayout<PickerOption>, 'ref' | 'horizontal' | 'data' | 'renderItem' | 'initialScrollIndex' | 'onScroll' | 'snapToOffsets' | 'contentContainerStyle' | 'getItemLayout'> {
|
|
5
|
+
}
|
|
3
6
|
export interface PickerOption {
|
|
4
7
|
label: string;
|
|
5
8
|
value: string | number;
|
|
6
9
|
}
|
|
7
|
-
interface
|
|
10
|
+
export interface PickerValues {
|
|
11
|
+
index: number;
|
|
12
|
+
value: PickerOption['value'];
|
|
13
|
+
}
|
|
14
|
+
export interface HorizontalPickerRef {
|
|
15
|
+
scrollToEnd: (params?: {
|
|
16
|
+
animated?: boolean | null;
|
|
17
|
+
}) => void;
|
|
18
|
+
scrollToIndex: (params: {
|
|
19
|
+
animated?: boolean | null;
|
|
20
|
+
index: number;
|
|
21
|
+
viewOffset?: number;
|
|
22
|
+
viewPosition?: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
scrollToItem: (params: {
|
|
25
|
+
animated?: boolean | null;
|
|
26
|
+
item: PickerOption;
|
|
27
|
+
viewOffset?: number;
|
|
28
|
+
viewPosition?: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
scrollToOffset: (params: {
|
|
31
|
+
animated?: boolean | null;
|
|
32
|
+
offset: number;
|
|
33
|
+
}) => void;
|
|
8
34
|
}
|
|
9
35
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
36
|
+
ref?: Ref<HorizontalPickerRef | null>;
|
|
10
37
|
items: PickerOption[];
|
|
11
38
|
initialScrollIndex?: number;
|
|
12
39
|
visibleItemCount?: number;
|
|
@@ -18,6 +45,6 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
18
45
|
pickerItemStyle?: ViewStyle;
|
|
19
46
|
pickerItemTextStyle?: TextStyle;
|
|
20
47
|
}
|
|
21
|
-
export declare function HorizontalPicker({ 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;
|
|
48
|
+
export declare function HorizontalPicker({ ref, items, initialScrollIndex, visibleItemCount, onChange, keyExtractor, scrollEventThrottle, decelerationRate, onLayout, showsHorizontalScrollIndicator, initialNumToRender, maxToRenderPerBatch, removeClippedSubviews, focusedTransformStyle, unfocusedTransformStyle, focusedOpacityStyle, unfocusedOpacityStyle, pickerItemStyle, pickerItemTextStyle, style, ...props }: HorizontalPickerProps): import("react").JSX.Element;
|
|
22
49
|
export {};
|
|
23
50
|
//# 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,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9D,aAAa,EAAE,CAAC,MAAM,EAAE;QACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,YAAY,EAAE,CAAC,MAAM,EAAE;QACrB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,IAAI,EAAE,YAAY,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,KAAK,IAAI,CAAC;IACX,cAAc,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACjF;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,GAAG,CAAC,EAAE,GAAG,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAC;IACtC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,qBAAqB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IAC/C,uBAAuB,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACjD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B,mBAAmB,CAAC,EAAE,SAAS,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,GAAG,EACH,KAAK,EACL,kBAAsB,EACtB,gBAAoB,EACpB,QAAQ,EACR,YAAwD,EACxD,mBAAwB,EACxB,gBAAyB,EACzB,QAAQ,EACR,8BAAsC,EACtC,kBAAuB,EACvB,mBAAwB,EACxB,qBAA4B,EAC5B,qBAAyC,EACzC,uBAAwC,EACxC,mBAAuB,EACvB,qBAA2B,EAC3B,eAAe,EACf,mBAAmB,EACnB,KAAK,EACL,GAAG,KAAK,EACT,EAAE,qBAAqB,+BAwGvB"}
|
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, pickerItemTextStyle, 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,6 +41,12 @@ export function HorizontalPicker({ items, initialScrollIndex = 0, visibleItemCou
|
|
|
43
41
|
runOnJS(handleOnChange)(safeIndex);
|
|
44
42
|
},
|
|
45
43
|
});
|
|
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
|
+
}), []);
|
|
46
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);
|
|
@@ -75,7 +79,6 @@ 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,
|
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, type TextStyle, 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\nexport interface 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 pickerItemTextStyle?: TextStyle;\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 pickerItemTextStyle,\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 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 height: 60,\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 Pressable,\n StyleSheet,\n type TextStyle,\n View,\n type ViewStyle,\n} from 'react-native';\nimport Animated, {\n type FlatListPropsWithLayout,\n type SharedValue,\n runOnJS,\n useAnimatedScrollHandler,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n} from 'react-native-reanimated';\n\ninterface FlatListProps\n extends Omit<\n FlatListPropsWithLayout<PickerOption>,\n | 'ref'\n | 'horizontal'\n | 'data'\n | 'renderItem'\n | 'initialScrollIndex'\n | 'onScroll'\n | 'snapToOffsets'\n | 'contentContainerStyle'\n | 'getItemLayout'\n > {}\n\nexport interface PickerOption {\n label: string;\n value: string | number;\n}\n\nexport interface PickerValues {\n index: number;\n value: PickerOption['value'];\n}\n\nexport interface HorizontalPickerRef {\n scrollToEnd: (params?: { animated?: boolean | null }) => void;\n scrollToIndex: (params: {\n animated?: boolean | null;\n index: number;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToItem: (params: {\n animated?: boolean | null;\n item: PickerOption;\n viewOffset?: number;\n viewPosition?: number;\n }) => void;\n scrollToOffset: (params: { animated?: boolean | null; offset: number }) => void;\n}\n\nexport interface HorizontalPickerProps extends FlatListProps {\n ref?: Ref<HorizontalPickerRef | null>;\n items: PickerOption[];\n initialScrollIndex?: number;\n visibleItemCount?: number;\n onChange?: (value: PickerOption['value'], index: number) => void;\n focusedTransformStyle?: ViewStyle['transform'];\n unfocusedTransformStyle?: ViewStyle['transform'];\n focusedOpacityStyle?: number;\n unfocusedOpacityStyle?: number;\n pickerItemStyle?: ViewStyle;\n pickerItemTextStyle?: TextStyle;\n}\n\nexport function HorizontalPicker({\n ref,\n items,\n initialScrollIndex = 0,\n visibleItemCount = 7,\n onChange,\n keyExtractor = (item, index) => `${item.value}-${index}`,\n scrollEventThrottle = 16,\n decelerationRate = 'fast',\n onLayout,\n showsHorizontalScrollIndicator = false,\n initialNumToRender = 15,\n maxToRenderPerBatch = 15,\n removeClippedSubviews = true,\n focusedTransformStyle = [{ scale: 1.15 }],\n unfocusedTransformStyle = [{ scale: 1 }],\n focusedOpacityStyle = 1,\n unfocusedOpacityStyle = 0.2,\n pickerItemStyle,\n pickerItemTextStyle,\n style,\n ...props\n}: HorizontalPickerProps) {\n const listRef = useRef<NativeFlatList<PickerOption> | null>(null);\n const [width, setWidth] = useState<number>(0);\n\n const currentIndex = useSharedValue<number>(initialScrollIndex);\n\n const { itemWidth, paddingSide } = useMemo(() => {\n const itemWidth = PixelRatio.roundToNearestPixel(width / visibleItemCount);\n const paddingSide = PixelRatio.roundToNearestPixel(width / 2 - itemWidth / 2);\n return {\n itemWidth: itemWidth,\n paddingSide: paddingSide,\n };\n }, [width, visibleItemCount]);\n\n const snapOffsets = useMemo(() => {\n return items.map((_, index) => PixelRatio.roundToNearestPixel(index * itemWidth));\n }, [items, itemWidth]);\n\n const handleOnChange = (index: number) => {\n const item = items[index];\n if (item) {\n onChange?.(item.value, index);\n }\n };\n\n const scrollToIndex = (index: number) => {\n listRef.current?.scrollToOffset({\n offset: index * itemWidth,\n animated: true,\n });\n };\n\n const onScroll = useAnimatedScrollHandler({\n onScroll: (e) => {\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n },\n onMomentumEnd: (e) => {\n const newIndex = Math.round(e.contentOffset.x / itemWidth);\n const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));\n currentIndex.value = safeIndex;\n runOnJS(handleOnChange)(safeIndex);\n },\n });\n\n useImperativeHandle(\n ref,\n () => ({\n scrollToEnd: (params) => listRef.current?.scrollToEnd(params),\n scrollToIndex: (params) => listRef.current?.scrollToIndex(params),\n scrollToItem: (params) => listRef.current?.scrollToItem(params),\n scrollToOffset: (params) => listRef.current?.scrollToOffset(params),\n }),\n [],\n );\n\n return (\n <Animated.FlatList\n {...props}\n ref={listRef}\n horizontal={true}\n data={items}\n keyExtractor={keyExtractor}\n renderItem={({ item, index }) => (\n <PickerItem\n label={item.label}\n index={index}\n itemWidth={itemWidth}\n currentIndex={currentIndex}\n onPress={() => scrollToIndex(index)}\n focusedTransformStyle={focusedTransformStyle}\n unfocusedTransformStyle={unfocusedTransformStyle}\n focusedOpacityStyle={focusedOpacityStyle}\n unfocusedOpacityStyle={unfocusedOpacityStyle}\n pickerItemStyle={pickerItemStyle}\n pickerItemTextStyle={pickerItemTextStyle}\n />\n )}\n onScroll={onScroll}\n scrollEventThrottle={scrollEventThrottle}\n decelerationRate={decelerationRate}\n onLayout={(e) => {\n if (typeof onLayout === 'function') {\n onLayout(e);\n }\n setWidth(e.nativeEvent.layout.width);\n }}\n showsHorizontalScrollIndicator={showsHorizontalScrollIndicator}\n snapToOffsets={snapOffsets}\n contentContainerStyle={{ paddingHorizontal: paddingSide }}\n getItemLayout={(_, index) => ({\n length: itemWidth,\n offset: itemWidth * index,\n index,\n })}\n initialScrollIndex={initialScrollIndex}\n initialNumToRender={initialNumToRender}\n maxToRenderPerBatch={maxToRenderPerBatch}\n removeClippedSubviews={removeClippedSubviews}\n style={[styles.container, style]}\n />\n );\n}\ninterface PickerItemProps\n extends Pick<\n HorizontalPickerProps,\n | 'focusedTransformStyle'\n | 'unfocusedTransformStyle'\n | 'focusedOpacityStyle'\n | 'unfocusedOpacityStyle'\n | 'pickerItemStyle'\n | 'pickerItemTextStyle'\n > {\n label: string;\n index: number;\n itemWidth: number;\n currentIndex: SharedValue<number>;\n onPress: () => void;\n}\n\nfunction PickerItem({\n label,\n index,\n itemWidth,\n currentIndex,\n onPress,\n focusedTransformStyle,\n unfocusedTransformStyle,\n focusedOpacityStyle,\n unfocusedOpacityStyle,\n pickerItemStyle,\n pickerItemTextStyle,\n}: PickerItemProps) {\n const isFocused = useDerivedValue(() => currentIndex.value === index);\n\n const animatedStyle = useAnimatedStyle(() => {\n return {\n transform: isFocused.value ? focusedTransformStyle : unfocusedTransformStyle,\n opacity: isFocused.value ? focusedOpacityStyle : unfocusedOpacityStyle,\n };\n }, [index]);\n\n return (\n <Pressable onPress={onPress}>\n <View style={[styles.itemContainer, { width: itemWidth }, pickerItemStyle]}>\n <Animated.Text style={[styles.itemText, animatedStyle, pickerItemTextStyle]}>{label}</Animated.Text>\n </View>\n </Pressable>\n );\n}\n\nconst styles = StyleSheet.create({\n container: {\n width: '100%',\n },\n itemContainer: {\n justifyContent: 'center',\n alignItems: 'center',\n },\n itemText: {\n fontSize: 13,\n fontWeight: '700',\n color: '#000000',\n },\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-horizontal-picker",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -54,7 +53,7 @@
|
|
|
54
53
|
},
|
|
55
54
|
"peerDependencies": {
|
|
56
55
|
"expo": "*",
|
|
57
|
-
"react": "
|
|
56
|
+
"react": "^19.0.0",
|
|
58
57
|
"react-native": "*",
|
|
59
58
|
"react-native-reanimated": ">=2.10.0"
|
|
60
59
|
},
|
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
|
-
export 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,35 @@ 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 PickerValues {
|
|
41
|
+
index: number;
|
|
42
|
+
value: PickerOption['value'];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface HorizontalPickerRef {
|
|
46
|
+
scrollToEnd: (params?: { animated?: boolean | null }) => void;
|
|
47
|
+
scrollToIndex: (params: {
|
|
48
|
+
animated?: boolean | null;
|
|
49
|
+
index: number;
|
|
50
|
+
viewOffset?: number;
|
|
51
|
+
viewPosition?: number;
|
|
52
|
+
}) => void;
|
|
53
|
+
scrollToItem: (params: {
|
|
54
|
+
animated?: boolean | null;
|
|
55
|
+
item: PickerOption;
|
|
56
|
+
viewOffset?: number;
|
|
57
|
+
viewPosition?: number;
|
|
58
|
+
}) => void;
|
|
59
|
+
scrollToOffset: (params: { animated?: boolean | null; offset: number }) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
export interface HorizontalPickerProps extends FlatListProps {
|
|
63
|
+
ref?: Ref<HorizontalPickerRef | null>;
|
|
33
64
|
items: PickerOption[];
|
|
34
65
|
initialScrollIndex?: number;
|
|
35
66
|
visibleItemCount?: number;
|
|
@@ -43,6 +74,7 @@ export interface HorizontalPickerProps extends FlatListProps {
|
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
export function HorizontalPicker({
|
|
77
|
+
ref,
|
|
46
78
|
items,
|
|
47
79
|
initialScrollIndex = 0,
|
|
48
80
|
visibleItemCount = 7,
|
|
@@ -64,10 +96,9 @@ export function HorizontalPicker({
|
|
|
64
96
|
style,
|
|
65
97
|
...props
|
|
66
98
|
}: HorizontalPickerProps) {
|
|
67
|
-
const listRef = useRef<
|
|
99
|
+
const listRef = useRef<NativeFlatList<PickerOption> | null>(null);
|
|
68
100
|
const [width, setWidth] = useState<number>(0);
|
|
69
101
|
|
|
70
|
-
const scrollX = useSharedValue<number>(0);
|
|
71
102
|
const currentIndex = useSharedValue<number>(initialScrollIndex);
|
|
72
103
|
|
|
73
104
|
const { itemWidth, paddingSide } = useMemo(() => {
|
|
@@ -99,7 +130,6 @@ export function HorizontalPicker({
|
|
|
99
130
|
|
|
100
131
|
const onScroll = useAnimatedScrollHandler({
|
|
101
132
|
onScroll: (e) => {
|
|
102
|
-
scrollX.value = e.contentOffset.x;
|
|
103
133
|
const newIndex = Math.round(e.contentOffset.x / itemWidth);
|
|
104
134
|
const safeIndex = Math.max(0, Math.min(items.length - 1, newIndex));
|
|
105
135
|
currentIndex.value = safeIndex;
|
|
@@ -112,6 +142,17 @@ export function HorizontalPicker({
|
|
|
112
142
|
},
|
|
113
143
|
});
|
|
114
144
|
|
|
145
|
+
useImperativeHandle(
|
|
146
|
+
ref,
|
|
147
|
+
() => ({
|
|
148
|
+
scrollToEnd: (params) => listRef.current?.scrollToEnd(params),
|
|
149
|
+
scrollToIndex: (params) => listRef.current?.scrollToIndex(params),
|
|
150
|
+
scrollToItem: (params) => listRef.current?.scrollToItem(params),
|
|
151
|
+
scrollToOffset: (params) => listRef.current?.scrollToOffset(params),
|
|
152
|
+
}),
|
|
153
|
+
[],
|
|
154
|
+
);
|
|
155
|
+
|
|
115
156
|
return (
|
|
116
157
|
<Animated.FlatList
|
|
117
158
|
{...props}
|
|
@@ -214,7 +255,6 @@ const styles = StyleSheet.create({
|
|
|
214
255
|
itemContainer: {
|
|
215
256
|
justifyContent: 'center',
|
|
216
257
|
alignItems: 'center',
|
|
217
|
-
height: 60,
|
|
218
258
|
},
|
|
219
259
|
itemText: {
|
|
220
260
|
fontSize: 13,
|