React Native自定义选择器

216 阅读2分钟

背景:由于公司需求提了一个时间选择器的功能,而参考了以下几个时间选择器库

  1. react-native-modal-datetime-picker
  2. react-native-month-year-picker
  3. react-native-picker-select

由于业务部分场景只需要年月的选择,以上三个库在本地实验后发现均无法满足业务需求。都是固定年月日,或者十分秒的选择功能。且有部分选择器调用的系统自带选择器,导致android与ios存在样式差异,于是决定自己基于ScrollView组件自定义写一个

import { View, StyleSheet, ScrollView, Animated, ViewStyle } from 'react-native';
import { memo, useEffect, useRef } from 'react';;

// 为了方便阅读,直接在当前页面展示
type DateSelectorProps = {
  data: string[];
  style?: ViewStyle;
  initialize?: any;
  /** 
   * 需要展示的数量 
   * ps: 必须为单数
  */
  visibleLength?: number;
  onChange: (value: any) => void;
}

const HEIGHT = 40;
/** 默认展示的数据数量 */
const VISIBLE_ITEMS = 5;

/** 自定义选择器 */
const CustomPicker = ({
  data,
  style,
  initialize,
  visibleLength,
  onChange,
}: DateSelectorProps) => {
  // 用来记录当前滚动位置Y值
  let scrollY = useRef(new Animated.Value(0)).current;
  // 现实的元素数量
  const visibleItems = useRef(visibleLength ?? VISIBLE_ITEMS).current;
  // 中间值
  const middle_index = useRef(Math.floor(visibleItems / 2)).current;
  const scroRef = useRef<any>();

  useEffect(() => {
    if (!initialize) return;
    // 如果传了初始值,初始化时定位到选中位置
    const index = data.findIndex(item => item === initialize);
    scroRef.current?.scrollTo({ y: index * HEIGHT, animated: false });
  }, []);

  return (
    <View style={[{ height: HEIGHT * visibleItems + 10 }, styles.container, style]}>
      <ScrollView
        ref={ref => scroRef.current = ref}
        snapToInterval={HEIGHT}
        snapToAlignment='start'
        onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], { useNativeDriver: false })}
        scrollEventThrottle={16}
        onMomentumScrollEnd={(event) => {
          const selectedIndex = Math.round(event.nativeEvent.contentOffset.y / HEIGHT);
          const selectedDate = data?.[selectedIndex];
          onChange(selectedDate);
        }}
      >
        {/* 兼容最前端与最末端数据选择, ps: 偷懒的写法,也可自行用其他方式实现 */}
        {['', '', ...data, '', '']?.map((date, index) => {
          const inputRange = [
            (index - middle_index - 2) * HEIGHT, 
            (index - middle_index - 1) * HEIGHT, 
            (index - middle_index) * HEIGHT, 
            (index - middle_index + 1) * HEIGHT, 
            (index - middle_index + 2) * HEIGHT];

          const fontSize = scrollY.interpolate({
            inputRange,
            outputRange: [14, 16, 18, 16, 14],
            extrapolate: 'clamp',
          });
          const color = scrollY.interpolate({
            inputRange,
            outputRange: ['#878787', '#565656', '#00CA63', '#565656', '#878787'],
            extrapolate: 'clamp',
          });
          const opacity = scrollY.interpolate({
            inputRange,
            outputRange: [0.6, 0.8, 1, 0.8, 0.6],
            extrapolate: 'clamp',
          });
          const rotateX = scrollY.interpolate({
            inputRange,
            outputRange: ['40deg', '30deg', '0deg', '30deg', '40deg'],
            extrapolate: 'clamp',
          });
          return (
            <View style={styles.item} key={index}>
              <Animated.Text style={{ fontSize, color, opacity, transform: [{ rotateX }] }}>{date}</Animated.Text>
            </View>
          );
        })}
      </ScrollView>
      <View pointerEvents='none' style={styles.line} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingVertical: 5,
    justifyContent: 'center',
  },
  item: {
    height: HEIGHT,
    justifyContent: 'center',
    alignItems: 'center'
  },
  line: {
    left: 0,
    right: 0,
    position: 'absolute',
    height: HEIGHT,
    borderTopColor: '#A7A7A7',
    borderTopWidth: 1,
    borderBottomColor: '#A7A7A7',
    borderBottomWidth: 1,
  },
});

export default memo(CustomPicker);