Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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