DEV Community

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

Posted on

5

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.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series 📺

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay