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

Change picker value onScroll — React Native and Expo.

sosog profile image Soso Gvritishvili ・4 min read

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;

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%',
  },
});

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.

Posted on by:

sosog profile

Soso Gvritishvili

@sosog

https://www.linkedin.com/in/ioseb-gvritishvili/

Discussion

markdown guide