Skip to content

Commit

Permalink
fix: 优化picker组件的性能问题
Browse files Browse the repository at this point in the history
  • Loading branch information
chj-damon committed Mar 25, 2024
1 parent 9ea47b1 commit a1ca23a
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 110 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-cheetahs-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@td-design/react-native-picker': minor
---

fix: 优化picker组件的性能问题
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const DatePicker: FC<
{...restProps}
data={col}
value={values[index]}
onChange={itemValue => onValueChange(itemValue, index)}
index={index}
onChange={onValueChange}
/>
);
})}
Expand Down
92 changes: 49 additions & 43 deletions packages/react-native-picker/src/components/WheelPicker/index.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -14,6 +23,7 @@ export default function WheelPicker({
itemStyle,
itemTextStyle,
itemHeight = 40,
index,
onChange,
}: WheelPickerProps) {
const theme = useTheme<Theme>();
Expand All @@ -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<NativeScrollEvent>) => {
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<NativeScrollEvent>) => {
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: {
Expand All @@ -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<OptionItem>) => {
return (
<WheelPickerItem
index={index}
option={option}
Expand All @@ -105,9 +110,8 @@ export default function WheelPicker({
currentIndex={currentScrollIndex}
visibleRest={2}
/>
),
[itemStyle, itemTextStyle, itemHeight, currentScrollIndex]
);
);
});

return (
<View style={[styles.container, containerStyle]}>
Expand All @@ -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}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/** 滚轮选择器子项的属性 */
Expand Down
22 changes: 13 additions & 9 deletions packages/react-native-picker/src/date-picker/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -59,17 +59,21 @@ const DatePicker = forwardRef<DatePickerRef, DatePickerProps>((props, ref) => {
submit: { width: '100%', justifyContent: 'center', alignItems: 'flex-end' },
});

const DatePickerComp = (
<DatePickerRN
{...restProps}
{...{ mode, value: date, minDate, maxDate, labelUnit, format }}
onChange={handleChange}
/>
);
const DatePickerComp = useMemo(() => {
if (!visible) return null;

return (
<DatePickerRN
{...restProps}
{...{ mode, value: date, minDate, maxDate, labelUnit, format }}
onChange={handleChange}
/>
);
}, [visible, date, mode, minDate, maxDate, labelUnit, format, restProps]);

if (displayType === 'modal') {
return (
<Modal visible={visible} onClose={handleClose} animationDuration={150}>
<Modal visible={visible} onClose={handleClose} animationDuration={0}>
<Flex
borderBottomWidth={ONE_PIXEL}
borderBottomColor="border"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { StyleSheet } from 'react-native';

import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native';
Expand Down Expand Up @@ -32,21 +32,26 @@ const Cascader = ({
onClose,
});

const PickerComp = (
<Flex backgroundColor="white">
{childrenTree.map((item: CascadePickerItemProps[] = [], level) => (
<WheelPicker
key={level}
{...{ data: item.map(el => ({ ...el, value: `${el.value}` })), value: `${stateValue[level]}` }}
onChange={val => handleValueChange(val, level)}
/>
))}
</Flex>
);
const PickerComp = useMemo(() => {
if (!visible) return null;
if (childrenTree.length === 0) return null;

return (
<Flex backgroundColor="white">
{childrenTree.map((item: CascadePickerItemProps[] = [], index) => (
<WheelPicker
key={index}
{...{ data: item.map(el => ({ ...el, value: `${el.value}` })), index, value: `${stateValue[index]}` }}
onChange={handleValueChange}
/>
))}
</Flex>
);
}, [visible, childrenTree, stateValue]);

if (displayType === 'modal') {
return (
<Modal visible={visible} onClose={onClose} animationDuration={150}>
<Modal visible={visible} onClose={onClose} animationDuration={0}>
<Flex
borderBottomWidth={ONE_PIXEL}
borderBottomColor="border"
Expand Down Expand Up @@ -86,4 +91,4 @@ const styles = StyleSheet.create({
submit: { width: '100%', justifyContent: 'center', alignItems: 'flex-end' },
});

export default React.memo(Cascader);
export default Cascader;
100 changes: 59 additions & 41 deletions packages/react-native-picker/src/picker/components/Normal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from 'react';
import React, { FC, useMemo } from 'react';
import { StyleSheet } from 'react-native';

import { Flex, helpers, Modal, Pressable, Text } from '@td-design/react-native';
Expand Down Expand Up @@ -32,57 +32,75 @@ const NormalPicker: FC<PickerProps & ModalPickerProps> = props => {
displayType,
});

const PickerComp = (
<Flex backgroundColor="white">
{pickerData.map((item, index) => (
<WheelPicker
key={index}
{...restProps}
{...{ data: item, value: selectedValue ? selectedValue[index] : '' }}
onChange={val => handleChange(val, index)}
/>
))}
</Flex>
);
const PickerComp = useMemo(() => {
if (!visible) return null;
if (pickerData.length === 0) return null;

if (pickerData.length === 1)
return (
<Flex backgroundColor="white">
<WheelPicker
{...restProps}
{...{ data: pickerData[0], index: 0, value: selectedValue?.[0] ?? '' }}
onChange={handleChange}
/>
</Flex>
);

return (
<Flex backgroundColor="white">
{pickerData.map((item, index) => (
<WheelPicker
key={index}
{...restProps}
{...{ data: item, index, value: selectedValue?.[index] ?? '' }}
onChange={handleChange}
/>
))}
</Flex>
);
}, [visible, pickerData, selectedValue, restProps]);

if (displayType === 'modal') {
return (
<Modal visible={visible} onClose={handleClose} animationDuration={150}>
<Flex
height={px(50)}
borderBottomWidth={ONE_PIXEL}
borderBottomColor="border"
backgroundColor="white"
paddingHorizontal="x3"
>
<Flex.Item alignItems="flex-start">
<Pressable activeOpacity={activeOpacity} onPress={handleClose} style={styles.cancel}>
<Text variant="p0" color="primary200">
{cancelText}
</Text>
</Pressable>
</Flex.Item>
<Flex.Item alignItems="center">
<Text variant="p0" color="text">
{title}
</Text>
</Flex.Item>
<Flex.Item alignItems="flex-end">
<Pressable activeOpacity={activeOpacity} onPress={handleOk} style={styles.submit}>
<Text variant="p0" color="primary200">
{okText}
<Modal visible={visible} onClose={handleClose} animationDuration={0}>
{
<Flex
height={px(50)}
borderBottomWidth={ONE_PIXEL}
borderBottomColor="border"
backgroundColor="white"
paddingHorizontal="x3"
>
<Flex.Item alignItems="flex-start">
<Pressable activeOpacity={activeOpacity} onPress={handleClose} style={styles.cancel}>
<Text variant="p0" color="primary200">
{cancelText}
</Text>
</Pressable>
</Flex.Item>
<Flex.Item alignItems="center">
<Text variant="p0" color="text">
{title}
</Text>
</Pressable>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item alignItems="flex-end">
<Pressable activeOpacity={activeOpacity} onPress={handleOk} style={styles.submit}>
<Text variant="p0" color="primary200">
{okText}
</Text>
</Pressable>
</Flex.Item>
</Flex>
}
{PickerComp}
</Modal>
);
}
return PickerComp;
};

export default React.memo(NormalPicker);
export default NormalPicker;

const styles = StyleSheet.create({
cancel: { width: '100%', justifyContent: 'center', alignItems: 'flex-start' },
Expand Down
Loading

0 comments on commit a1ca23a

Please sign in to comment.