DEV Community

Cover image for Change picker value onScroll — React Native and Expo.
Soso Gvritishvili
Soso Gvritishvili

Posted on

Change picker value onScroll — React Native and Expo.

Default NPM package example
Default NPM package example

This was my first React native project for a property development company and the task was to highlight the selected building floor on a wheel picker scroll. (By the way, you can check the working example of this APP (IOS, Android), for now, the language is only Georgian.) U also can download this package from NPM

Working project example
Working project example

But there was no hope of finding any react-native package or StackOverflow help. All custom pickers and IOS native pickers too are making callback only on scroll end. I always try to write own code and not use packages, but this time I thought that is was a difficult task which will take a lot of time. The hours and energy spent on the search told me that I had to do everything by myself. Fortunately, many React Native developers were looking for similar functionality and in their google footsteps, I found the react-native-swipe-picker package, where FlatList or ScrollView have been used as a picker, so this was a chance to fix my issue.

I have added scroll callbacks, fixed some bugs, and improved the functionality, make it more convenient for developers.

A simple example of how to use DynamicallySelectedPicker component

import React, {useState} from 'react'; import {StyleSheet, View, Text} from 'react-native'; import {Colors} from 'react-native/Libraries/NewAppScreen'; import DynamicallySelectedPicker from './src/components/DynamicallySelectedPicker'; const App = () => { const [selectedValue, setSelectedValue] = useState(0); return ( <View style={styles.body}> <View style={{margin: 30}}> <Text>Item index {selectedValue}</Text>  </View>  <DynamicallySelectedPicker items={[ { value: 1, label: 'Item 1', }, { value: 2, label: 'Item 2', }, { value: 3, label: 'Item 3', }, { value: 4, label: 'Item 4', }, { value: 5, label: 'Item 5', }, ]} width={300} height={300} onScroll={(selected) => setSelectedValue(selected.index)} />  </View>  ); }; const styles = StyleSheet.create({ body: { backgroundColor: Colors.white, flex: 1, alignItems: 'center', justifyContent: 'center', }, }); export default App; 
Enter fullscreen mode Exit fullscreen mode

This is a React Native example with one big component (it could be separated into little functional components). To run it with Expo you have to change

react-native-linear-gradient package to expo-linear-gradient

import React from 'react'; import PropTypes from 'prop-types'; import {StyleSheet, View, ScrollView, Platform, Text} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import PickerListItem from './PickerListItem'; export default class DynamicallySelectedPicker extends React.Component { constructor(props) { super(props); // set picker item height for android and ios const {height, transparentItemRows, initialSelectedIndex} = props; let itemHeight = height / (transparentItemRows * 2 + 1); // In ios we have to manually ceil items height to eliminate distortion in the visualization, when we have big data. if (Platform.OS === 'ios') { itemHeight = Math.ceil(itemHeight); } this.state = { itemHeight: itemHeight, itemIndex: initialSelectedIndex, }; } /** * Generate fake items for picker top and bottom. * @param n * @returns {[]} */ fakeItems(n = 3) { const itemsArr = []; for (let i = 0; i < n; i++) { itemsArr[i] = { value: -1, label: '', }; } return itemsArr; } /** * Get extended picker items length. * @returns {number} */ allItemsLength() { return this.extendedItems().length - this.props.transparentItemRows * 2; } /** * * @param event */ onScroll(event) { const {items, onScroll} = this.props; const tempIndex = this.getItemTemporaryIndex(event); if ( this.state.itemIndex !== tempIndex && tempIndex >= 0 && tempIndex < this.allItemsLength() ) { this.setItemIndex(tempIndex); onScroll({index: tempIndex, item: items[tempIndex]}); } } /** * * @param event * @returns {number} */ getItemTemporaryIndex(event) { return Math.round( event.nativeEvent.contentOffset.y / this.state.itemHeight, ); } /** * * @param index */ setItemIndex(index) { this.setState({ itemIndex: index, }); } /** * Add fake items to make picker almost like IOS native wheel picker. * @returns {*[]} */ extendedItems() { const {transparentItemRows} = this.props; return [ ...this.fakeItems(transparentItemRows), ...this.props.items, ...this.fakeItems(transparentItemRows), ]; } /** * * @param item * @param index * @returns {*} */ renderPickerListItem(item, index) { const {itemHeight} = this.state; const {allItemsColor, itemColor} = this.props; return ( <View key={index} style={[ styles.listItem, { height: itemHeight, }, ]}> <Text style={{ color: itemColor ? itemColor : allItemsColor, }}> {item.label} </Text>  </View>  ); } render() { const {itemIndex, itemHeight} = this.state; const { width, height, topGradientColors, bottomGradientColors, selectedItemBorderColor, transparentItemRows, } = this.props; return ( <View style={{height: height, width: width}}> <ScrollView showsVerticalScrollIndicator={false} showsHorizontalScrollIndicator={false} onScroll={(event) => { this.onScroll(event); }} scrollEventThrottle initialScrollIndex={itemIndex} snapToInterval={itemHeight}> {this.extendedItems().map((item, index) => { return this.renderPickerListItem(item, index); })} </ScrollView>  <View style={[ styles.gradientWrapper, { top: 0, borderBottomWidth: 1, borderBottomColor: selectedItemBorderColor, }, ]} pointerEvents="none"> <LinearGradient colors={topGradientColors} style={[ styles.pickerGradient, { height: transparentItemRows * itemHeight, }, ]} />  </View>  <View style={[ styles.gradientWrapper, { bottom: 0, borderTopWidth: 1, borderTopColor: selectedItemBorderColor, }, ]} pointerEvents="none"> <LinearGradient colors={bottomGradientColors} style={[ styles.pickerGradient, {height: transparentItemRows * itemHeight}, ]} />  </View>  </View>  ); } } DynamicallySelectedPicker.defaultProps = { items: [{value: 0, label: 'No items', itemColor: 'red'}], onScroll: () => {}, width: 300, height: 300, initialSelectedIndex: 0, transparentItemRows: 3, allItemsColor: '#000', selectedItemBorderColor: '#cecece', topGradientColors: [ 'rgba( 255, 255, 255, 1 )', 'rgba( 255, 255, 255, 0.9 )', 'rgba( 255, 255, 255, 0.7 )', 'rgba( 255, 255, 255, 0.5 )', ], bottomGradientColors: [ 'rgba( 255, 255, 255, 0.5 )', 'rgba( 255, 255, 255, 0.7 )', 'rgba( 255, 255, 255, 0.9 )', 'rgba( 255, 255, 255, 1 )', ], }; DynamicallySelectedPicker.propTypes = { items: PropTypes.arrayOf( PropTypes.shape({ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), label: PropTypes.string, itemColor: PropTypes.string, }), ), onScroll: PropTypes.func, initialSelectedIndex: PropTypes.number, height: PropTypes.number, width: PropTypes.number, allItemsColor: PropTypes.string, selectedItemBorderColor: PropTypes.string, topGradientColors: PropTypes.array, bottomGradientColors: PropTypes.array, }; const styles = StyleSheet.create({ listItem: { alignItems: 'center', justifyContent: 'center', }, gradientWrapper: { position: 'absolute', width: '100%', }, pickerGradient: { width: '100%', }, }); 
Enter fullscreen mode Exit fullscreen mode

I hope someone will use it and won’t waste time like me. Also if u have questions or remarks, please, feel free to communicate.

Contribution or issues reports in the GitHub repository (It’s a little bit different, with more props and callbacks.) would be great.

Sorry again for my bad English and thank you.

Top comments (0)