背景:由于公司需求提了一个时间选择器的功能,而参考了以下几个时间选择器库
- react-native-modal-datetime-picker
- react-native-month-year-picker
- 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);