├── .gitignore
├── demo.gif
├── tsconfig.json
├── LICENSE
├── package.json
├── README.md
└── index.tsx
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules
3 | .DS_Store
4 | .swp
5 | .idea
6 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rheng001/react-native-wheel-scrollview-picker/HEAD/demo.gif
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules", "./node_modules", "./node_modules/*"],
3 | "declaration": "true",
4 | "compilerOptions": {
5 | "allowSyntheticDefaultImports": true,
6 | "allowJs": true,
7 | "esModuleInterop": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "isolatedModules": true,
10 | "jsx": "preserve",
11 | "lib": ["dom", "dom.iterable", "esnext", "es6"],
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "noEmit": true,
15 | "noImplicitAny": true,
16 | "noImplicitReturns": true,
17 | "noUnusedParameters": true,
18 | "resolveJsonModule": true,
19 | "skipDefaultLibCheck": true,
20 | "skipLibCheck": true,
21 | "strict": true,
22 | "strictNullChecks": true,
23 | "target": "es5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Richard Heng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-wheel-scrollview-picker",
3 | "version": "2.0.9",
4 | "description": "A pure js picker for React Native",
5 | "main": "index.tsx",
6 | "scripts": {
7 | "test": "echo \"No test specified\"",
8 | "prepublish": "tsc"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/rheng001/react-native-wheel-scrollview-picker.git"
13 | },
14 | "keywords": [
15 | "picker",
16 | "wheel",
17 | "react-native",
18 | "react-native-picker"
19 | ],
20 | "author": "rheng001",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/rheng001/react-native-wheel-scrollview-picker/issues"
24 | },
25 | "homepage": "https://github.com/rheng001/react-native-wheel-scrollview-picker#readme",
26 | "peerDependencies": {
27 | "@types/react": "*",
28 | "@types/react-native": "*",
29 | "react": "*",
30 | "react-native": "*",
31 | "typescript": "*"
32 | },
33 | "devDependencies": {
34 | "@types/react": "^17.0.15",
35 | "@types/react-native": "^0.64.12",
36 | "react": "^17.0.2",
37 | "react-native": "^0.64.2",
38 | "typescript": "^4.3.5"
39 | },
40 | "types": "index.d.ts"
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
react-native-wheel-scrollview-picker
2 |
3 |
4 |
5 |
6 |
7 | A pure js picker for React Native
8 |
9 |
10 |
11 | > - Original repository by @veizz: [react-native-picker-scrollview](https://github.com/veizz/react-native-picker-scrollview).
12 | > - Fork by @yasemincidem who added the real cross platform behavior and datepicker [react-native-wheel-scroll-picker](https://github.com/yasemincidem/react-native-picker-scrollview).
13 | > - This is the third fork of repository, since it seems that @yasemincidem is no longer supporting [react-native-wheel-scroll-picker](https://github.com/yasemincidem/react-native-picker-scrollview).
14 |
15 | ---
16 |
17 | ## Table of Contents
18 |
19 | 1. [Features](#features)
20 | 2. [Installation](#installation)
21 | 3. [Usage](#usage)
22 | - [Example](#usage)
23 | 4. [Props](#props)
24 | 5. [License](#license)
25 |
26 | ## Installation
27 |
28 | ```sh
29 | yarn add react-native-wheel-scrollview-picker
30 | # or
31 | npm install react-native-wheel-scrollview-picker --save
32 | ```
33 |
34 | ## Usage
35 |
36 | ```jsx
37 | import React, { Component } from "react";
38 | import ScrollPicker from "react-native-wheel-scrollview-picker";
39 |
40 | export default class SimpleExample extends Component {
41 | render() {
42 | return (
43 | {
47 | //
48 | }}
49 | onValueChange={(data, selectedIndex) => {
50 | //
51 | }}
52 | wrapperHeight={180}
53 | wrapperBackground="#FFFFFF"
54 | itemHeight={60}
55 | highlightColor="#d8d8d8"
56 | highlightBorderWidth={2}
57 | />
58 | );
59 | }
60 | }
61 | ```
62 |
63 | ## Props
64 |
65 | | Props | Description | Type | Default |
66 | | -------------------- | :---------------------------: | :----: | --------: |
67 | | dataSource | Data of the picker | Array | |
68 | | selectedIndex | selected index of the item | number | 1 |
69 | | wrapperHeight | height of the picker | number | |
70 | | wrapperBackground | picker background | string | '#FFF' |
71 | | itemHeight | height of each item | number | |
72 | | highlightColor | color of the indicator line | number | "#d8d8d8" |
73 | | highlightBorderWidth | width of the indicator | string | 1 |
74 | | activeItemTextStyle | Active Item Text object style | object | |
75 | | itemTextStyle | Item Text object style | object | |
76 |
77 | ## Extra
78 |
79 | If you want to scroll to target index, you need the instance function, and that is exposed some functions to parent components by using `useImperativeHandle` ,you can use it。
80 |
81 | ```jsx
82 | import React from "react";
83 | import { Button } from 'react-native';
84 | import ScrollPicker from "react-native-wheel-scrollview-picker";
85 |
86 | const dataSource = ["1", "2", "3", "4", "5", "6"]
87 | export const Demo = () => {
88 | const ref = React.useRef();
89 | const [index, setIndex] = React.useState(0);
90 | const onValueChange = (data, selectedIndex) => {
91 | setIndex(selectedIndex);
92 | };
93 |
94 | const onNext = () => {
95 | if (index === dataSource.length - 1) return;
96 | setIndex(index + 1);
97 | ref.current && ref.current.scrollToTargetIndex(index + 1);
98 | }
99 | return (
100 |
105 |
109 | );
110 | };
111 | ```
112 |
113 | ## Author
114 |
115 | - [Richard Heng](http://richardheng.me/)
116 |
117 | ## License
118 |
119 | MIT
120 |
--------------------------------------------------------------------------------
/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useCallback,
3 | useEffect,
4 | useRef,
5 | useState,
6 | useImperativeHandle,
7 | ReactNode,
8 | Ref,
9 | } from "react";
10 | import {
11 | Dimensions,
12 | NativeScrollEvent,
13 | NativeSyntheticEvent,
14 | Platform,
15 | ScrollView,
16 | ScrollViewProps,
17 | StyleSheet,
18 | Text,
19 | View,
20 | ViewProps,
21 | ViewStyle,
22 | } from "react-native";
23 |
24 | function isNumeric(str: string | unknown): boolean {
25 | if (typeof str === "number") return true;
26 | if (typeof str !== "string") return false;
27 | return (
28 | !isNaN(str as unknown as number) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
29 | !isNaN(parseFloat(str))
30 | ); // ...and ensure strings of whitespace fail
31 | }
32 |
33 | const deviceWidth = Dimensions.get("window").width;
34 |
35 | const isViewStyle = (style: ViewProps["style"]): style is ViewStyle => {
36 | return (
37 | typeof style === "object" &&
38 | style !== null &&
39 | Object.keys(style).includes("height")
40 | );
41 | };
42 |
43 | export type ScrollPickerProps = {
44 | style?: ViewProps["style"];
45 | dataSource: Array;
46 | selectedIndex?: number;
47 | onValueChange?: (
48 | value: ItemT | undefined,
49 | index: number
50 | ) => void;
51 | renderItem?: (
52 | data: ItemT,
53 | index: number,
54 | isSelected: boolean
55 | ) => JSX.Element;
56 | highlightColor?: string;
57 | highlightBorderWidth?: number;
58 | itemTextStyle?: object;
59 | activeItemTextStyle?: object;
60 | itemHeight?: number;
61 | wrapperHeight?: number;
62 | wrapperBackground?: string;
63 | // TODO: add proper type to `scrollViewComponent` prop
64 | // tried using ComponentType }>
65 | // but ScrollView component from react-native-gesture=handler is not compatible with this.
66 | scrollViewComponent?: any;
67 | } & ScrollViewProps;
68 |
69 | export type ScrollPickerHandle = {
70 | scrollToTargetIndex: (val: number) => void;
71 | }
72 |
73 | const ScrollPicker: { (props: ScrollPickerProps & { ref?: Ref }): ReactNode } = React.forwardRef((propsState, ref) => {
74 | const { itemHeight = 30, style, scrollViewComponent, ...props } = propsState;
75 | const [initialized, setInitialized] = useState(false);
76 | const [selectedIndex, setSelectedIndex] = useState(
77 | props.selectedIndex && props.selectedIndex >= 0 ? props.selectedIndex : 0
78 | );
79 | const sView = useRef(null);
80 | const [isScrollTo, setIsScrollTo] = useState(false);
81 | const [dragStarted, setDragStarted] = useState(false);
82 | const [momentumStarted, setMomentumStarted] = useState(false);
83 | const [timer, setTimer] = useState(null);
84 |
85 | useImperativeHandle(ref, () => ({
86 | scrollToTargetIndex: (val: number) => {
87 | setSelectedIndex(val);
88 | sView?.current?.scrollTo({ y: val * itemHeight });
89 | },
90 | }));
91 |
92 | const wrapperHeight =
93 | props.wrapperHeight ||
94 | (isViewStyle(style) && isNumeric(style.height)
95 | ? Number(style.height)
96 | : 0) ||
97 | itemHeight * 5;
98 |
99 | useEffect(
100 | function initialize() {
101 | if (initialized) return;
102 | setInitialized(true);
103 |
104 | setTimeout(() => {
105 | const y = itemHeight * selectedIndex;
106 | sView?.current?.scrollTo({ y: y });
107 | }, 0);
108 |
109 | return () => {
110 | timer && clearTimeout(timer);
111 | };
112 | },
113 | [initialized, itemHeight, selectedIndex, sView, timer]
114 | );
115 |
116 | const renderPlaceHolder = () => {
117 | const h = (wrapperHeight - itemHeight) / 2;
118 | const header = ;
119 | const footer = ;
120 | return { header, footer };
121 | };
122 |
123 | const renderItem = (
124 | data: typeof props.dataSource[0],
125 | index: number
126 | ) => {
127 | const isSelected = index === selectedIndex;
128 | const item = props.renderItem ? (
129 | props.renderItem(data, index, isSelected)
130 | ) : (
131 |
142 | {data}
143 |
144 | );
145 |
146 | return (
147 |
148 | {item}
149 |
150 | );
151 | };
152 | const scrollFix = useCallback(
153 | (e: NativeSyntheticEvent) => {
154 | let y = 0;
155 | const h = itemHeight;
156 | if (e.nativeEvent.contentOffset) {
157 | y = e.nativeEvent.contentOffset.y;
158 | }
159 | const _selectedIndex = Math.round(y / h);
160 |
161 | const _y = _selectedIndex * h;
162 | if (_y !== y) {
163 | // using scrollTo in ios, onMomentumScrollEnd will be invoked
164 | if (Platform.OS === "ios") {
165 | setIsScrollTo(true);
166 | }
167 | sView?.current?.scrollTo({ y: _y });
168 | }
169 | if (selectedIndex === _selectedIndex) {
170 | return;
171 | }
172 | // onValueChange
173 | if (props.onValueChange) {
174 | const selectedValue = props.dataSource[_selectedIndex];
175 | setSelectedIndex(_selectedIndex);
176 | props.onValueChange(selectedValue, _selectedIndex);
177 | }
178 | },
179 | [itemHeight, props, selectedIndex]
180 | );
181 |
182 | const onScrollBeginDrag = () => {
183 | setDragStarted(true);
184 |
185 | if (Platform.OS === "ios") {
186 | setIsScrollTo(false);
187 | }
188 | timer && clearTimeout(timer);
189 | };
190 |
191 | const onScrollEndDrag = (e: NativeSyntheticEvent) => {
192 | setDragStarted(false);
193 |
194 | // if not used, event will be garbaged
195 | const _e: NativeSyntheticEvent = { ...e };
196 | timer && clearTimeout(timer);
197 | setTimer(
198 | setTimeout(() => {
199 | if (!momentumStarted) {
200 | scrollFix(_e);
201 | }
202 | }, 50)
203 | );
204 | };
205 | const onMomentumScrollBegin = () => {
206 | setMomentumStarted(true);
207 | timer && clearTimeout(timer);
208 | };
209 |
210 | const onMomentumScrollEnd = (e: NativeSyntheticEvent) => {
211 | setMomentumStarted(false);
212 |
213 | if (!isScrollTo && !dragStarted) {
214 | scrollFix(e);
215 | }
216 | };
217 |
218 | const { header, footer } = renderPlaceHolder();
219 | const highlightWidth = (isViewStyle(style) ? style.width : 0) || deviceWidth;
220 | const highlightColor = props.highlightColor || "#333";
221 | const highlightBorderWidth =
222 | props.highlightBorderWidth ?? StyleSheet.hairlineWidth;
223 |
224 | const wrapperStyle: ViewStyle = {
225 | height: wrapperHeight,
226 | flex: 1,
227 | backgroundColor: props.wrapperBackground || "#fafafa",
228 | overflow: "hidden",
229 | };
230 |
231 | const highlightStyle: ViewStyle = {
232 | position: "absolute",
233 | top: (wrapperHeight - itemHeight) / 2,
234 | height: itemHeight,
235 | width: highlightWidth,
236 | borderTopColor: highlightColor,
237 | borderBottomColor: highlightColor,
238 | borderTopWidth: highlightBorderWidth,
239 | borderBottomWidth: highlightBorderWidth,
240 | };
241 |
242 | const CustomScrollViewComponent = scrollViewComponent || ScrollView;
243 |
244 | return (
245 |
246 |
247 | onMomentumScrollBegin()}
253 | onMomentumScrollEnd={(e: NativeSyntheticEvent) =>
254 | onMomentumScrollEnd(e)
255 | }
256 | onScrollBeginDrag={(_e: any) => onScrollBeginDrag()}
257 | onScrollEndDrag={(e: NativeSyntheticEvent) =>
258 | onScrollEndDrag(e)
259 | }
260 | {...props}
261 | >
262 | {header}
263 | {props.dataSource.map(renderItem)}
264 | {footer}
265 |
266 |
267 | );
268 | });
269 | export default ScrollPicker;
270 |
271 | const styles = StyleSheet.create({
272 | itemWrapper: {
273 | height: 30,
274 | justifyContent: "center",
275 | alignItems: "center",
276 | },
277 | itemTextStyle: {
278 | color: "#999",
279 | },
280 | activeItemTextStyle: {
281 | color: "#333",
282 | },
283 | });
284 |
--------------------------------------------------------------------------------