diff --git a/.changeset/big-cheetahs-smell.md b/.changeset/big-cheetahs-smell.md new file mode 100644 index 0000000000..afabe95a11 --- /dev/null +++ b/.changeset/big-cheetahs-smell.md @@ -0,0 +1,5 @@ +--- +'@td-design/react-native-picker': minor +--- + +fix: 优化picker组件的性能问题 diff --git a/packages/react-native-picker/src/components/DatePicker/index.tsx b/packages/react-native-picker/src/components/DatePicker/index.tsx index 4ce8d1c054..4620df9328 100644 --- a/packages/react-native-picker/src/components/DatePicker/index.tsx +++ b/packages/react-native-picker/src/components/DatePicker/index.tsx @@ -32,7 +32,8 @@ const DatePicker: FC< {...restProps} data={col} value={values[index]} - onChange={itemValue => onValueChange(itemValue, index)} + index={index} + onChange={onValueChange} /> ); })} diff --git a/packages/react-native-picker/src/components/WheelPicker/index.tsx b/packages/react-native-picker/src/components/WheelPicker/index.tsx index 564c727fa2..16b98cb0e0 100644 --- a/packages/react-native-picker/src/components/WheelPicker/index.tsx +++ b/packages/react-native-picker/src/components/WheelPicker/index.tsx @@ -1,9 +1,18 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import { Animated, FlatList, NativeScrollEvent, NativeSyntheticEvent, StyleSheet, View } from 'react-native'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { + Animated, + FlatList, + ListRenderItemInfo, + NativeScrollEvent, + NativeSyntheticEvent, + StyleSheet, + View, +} from 'react-native'; import { Theme, useTheme } from '@td-design/react-native'; +import { useMemoizedFn } from '@td-design/rn-hooks'; -import { WheelPickerProps } from './type'; +import { OptionItem, WheelPickerProps } from './type'; import WheelPickerItem from './WheelPickerItem'; export default function WheelPicker({ @@ -14,6 +23,7 @@ export default function WheelPicker({ itemStyle, itemTextStyle, itemHeight = 40, + index, onChange, }: WheelPickerProps) { const theme = useTheme(); @@ -23,56 +33,44 @@ export default function WheelPicker({ const containerHeight = 5 * itemHeight; - const paddedOptions = useMemo(() => { + const { paddedOptions, offsets } = useMemo(() => { const array = [...data]; for (let i = 0; i < 2; i++) { array.unshift(undefined); array.push(undefined); } - return array; - }, [data]); + return { + paddedOptions: array, + offsets: array.map((_, i) => i * itemHeight), + }; + }, [data, itemHeight]); let selectedIndex = data.findIndex(item => item?.value === value); if (selectedIndex === -1) { selectedIndex = 0; } - const offsets = useMemo( - () => [...Array(paddedOptions.length)].map((_, i) => i * itemHeight), - [paddedOptions, itemHeight] - ); - const currentScrollIndex = Animated.add(Animated.divide(scrollY, itemHeight), 2); - const handleMomentumScrollBegin = useCallback(() => { + const handleMomentumScrollBegin = useMemoizedFn(() => { signal.current = false; - }, []); - - const handleMomentumScrollEnd = useCallback( - (event: NativeSyntheticEvent) => { - if (signal.current) return; - signal.current = true; + }); - // Due to list bounciness when scrolling to the start or the end of the list - // the offset might be negative or over the last item. - // We therefore clamp the offset to the supported range. - const offsetY = Math.min(itemHeight * (data.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0)); - let index = Math.ceil(offsetY / itemHeight) + 1; + const handleMomentumScrollEnd = useMemoizedFn((event: NativeSyntheticEvent) => { + if (signal.current) return; + signal.current = true; - const currentItem = data[index - 1]; - if (currentItem) { - onChange(currentItem.value); - } - }, - [itemHeight, data] - ); + // Due to list bounciness when scrolling to the start or the end of the list + // the offset might be negative or over the last item. + // We therefore clamp the offset to the supported range. + const offsetY = Math.min(itemHeight * (data.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0)); + const _index = Math.ceil(offsetY / itemHeight) + 1; - useEffect(() => { - flatListRef.current?.scrollToIndex({ - index: selectedIndex, - animated: false, - }); - }, [selectedIndex]); + const currentItem = data[_index - 1]; + if (currentItem) { + onChange(currentItem.value, index); + } + }); const styles = StyleSheet.create({ container: { @@ -94,8 +92,15 @@ export default function WheelPicker({ }, }); - const renderItem = useCallback( - ({ item: option, index }) => ( + useEffect(() => { + flatListRef.current?.scrollToIndex({ + index: selectedIndex, + animated: false, + }); + }, [selectedIndex]); + + const renderItem = useMemoizedFn(({ item: option, index }: ListRenderItemInfo) => { + return ( - ), - [itemStyle, itemTextStyle, itemHeight, currentScrollIndex] - ); + ); + }); return ( @@ -122,16 +126,18 @@ export default function WheelPicker({ onMomentumScrollBegin={handleMomentumScrollBegin} onMomentumScrollEnd={handleMomentumScrollEnd} snapToOffsets={offsets} - decelerationRate={'normal'} - initialScrollIndex={selectedIndex} + decelerationRate={'fast'} getItemLayout={(_, index) => ({ length: itemHeight, offset: itemHeight * index, index, })} + bounces={false} data={paddedOptions} keyExtractor={(_, index) => index.toString()} renderItem={renderItem} + maxToRenderPerBatch={3} + initialNumToRender={2} /> ); diff --git a/packages/react-native-picker/src/components/WheelPicker/type.ts b/packages/react-native-picker/src/components/WheelPicker/type.ts index 9239513b6d..7a5c058883 100644 --- a/packages/react-native-picker/src/components/WheelPicker/type.ts +++ b/packages/react-native-picker/src/components/WheelPicker/type.ts @@ -25,12 +25,13 @@ export interface WheelPickerPropsBase { /** 滚轮选择器的属性 */ export interface WheelPickerProps extends WheelPickerPropsBase { + index: number; /** 数据行数组 */ data: (CascadePickerItemProps | undefined)[]; /** 当前选中的数据行下标 */ value: ItemValue; /** 选择数据行的处理函数 */ - onChange: (value: ItemValue) => void; + onChange: (value: ItemValue, index: number) => void; } /** 滚轮选择器子项的属性 */ diff --git a/packages/react-native-picker/src/date-picker/index.tsx b/packages/react-native-picker/src/date-picker/index.tsx index ad731ddc61..4d7b3773b7 100644 --- a/packages/react-native-picker/src/date-picker/index.tsx +++ b/packages/react-native-picker/src/date-picker/index.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useImperativeHandle } from 'react'; +import React, { forwardRef, useImperativeHandle, useMemo } from 'react'; import { StyleSheet } from 'react-native'; import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native'; @@ -59,17 +59,21 @@ const DatePicker = forwardRef((props, ref) => { submit: { width: '100%', justifyContent: 'center', alignItems: 'flex-end' }, }); - const DatePickerComp = ( - - ); + const DatePickerComp = useMemo(() => { + if (!visible) return null; + + return ( + + ); + }, [visible, date, mode, minDate, maxDate, labelUnit, format, restProps]); if (displayType === 'modal') { return ( - + - {childrenTree.map((item: CascadePickerItemProps[] = [], level) => ( - ({ ...el, value: `${el.value}` })), value: `${stateValue[level]}` }} - onChange={val => handleValueChange(val, level)} - /> - ))} - - ); + const PickerComp = useMemo(() => { + if (!visible) return null; + if (childrenTree.length === 0) return null; + + return ( + + {childrenTree.map((item: CascadePickerItemProps[] = [], index) => ( + ({ ...el, value: `${el.value}` })), index, value: `${stateValue[index]}` }} + onChange={handleValueChange} + /> + ))} + + ); + }, [visible, childrenTree, stateValue]); if (displayType === 'modal') { return ( - + = props => { displayType, }); - const PickerComp = ( - - {pickerData.map((item, index) => ( - handleChange(val, index)} - /> - ))} - - ); + const PickerComp = useMemo(() => { + if (!visible) return null; + if (pickerData.length === 0) return null; + + if (pickerData.length === 1) + return ( + + + + ); + + return ( + + {pickerData.map((item, index) => ( + + ))} + + ); + }, [visible, pickerData, selectedValue, restProps]); if (displayType === 'modal') { return ( - - - - - - {cancelText} - - - - - - {title} - - - - - - {okText} + + { + + + + + {cancelText} + + + + + + {title} - - - + + + + + {okText} + + + + + } {PickerComp} ); @@ -82,7 +100,7 @@ const NormalPicker: FC = props => { return PickerComp; }; -export default React.memo(NormalPicker); +export default NormalPicker; const styles = StyleSheet.create({ cancel: { width: '100%', justifyContent: 'center', alignItems: 'flex-start' }, diff --git a/packages/react-native-picker/src/picker/components/Normal/useNormalPicker.ts b/packages/react-native-picker/src/picker/components/Normal/useNormalPicker.ts index 3f7a7b8402..96bc50d9b9 100644 --- a/packages/react-native-picker/src/picker/components/Normal/useNormalPicker.ts +++ b/packages/react-native-picker/src/picker/components/Normal/useNormalPicker.ts @@ -42,7 +42,7 @@ export default function useNormalPicker({ displayType, }: PickerProps & ModalPickerProps) { const { pickerData, initialValue } = useMemo(() => transform(data), [data]); - const [selectedValue, selectValue] = useSafeState(getValue(value, initialValue)); + const [selectedValue, selectValue] = useSafeState(undefined); useEffect(() => { selectValue(getValue(value, initialValue));