Реализация вертикального скролла для выбора времени в React Native

Пытаюсь реализовать свой компонет Time picker, и столкнулся с проблемой, что активный элемент после скрола меняет свой цвет с задержкой:

введите сюда описание изображения

(как видно на скриншоте активный элемент изменен на 55, а цвет остается на предыдущем элементе еще где-то 0.5-0.7 с.)

Есть ли какой-то способ избежать этого?

Вот мой код:

import React, { useState, useRef, useEffect } from 'react';
import {
    View,
    FlatList,
    StyleSheet,
    Text,
    NativeSyntheticEvent,
    NativeScrollEvent
} from 'react-native';
import { CustomModal } from '@shared/ui';
import { COLORS } from '@shared/styles';

const ITEM_HEIGHT = 40;
const VISIBLE_ITEMS = 5;

interface TimePickerModalProps {
    visible: boolean;
    onClose: () => void;
    onConfirm: (hours: string, minutes: string) => void;
}

const generateItems = (repeatCount: number, maxValue: number) => {
    const items = Array.from({ length: maxValue }, (_, i) => i.toString().padStart(2, '0'));
    return Array(repeatCount).fill(items).flat();
};

const REPEAT_COUNT = 5;
const hoursList = generateItems(REPEAT_COUNT, 24);
const minutesList = generateItems(REPEAT_COUNT, 60);

const TimePickerModal: React.FC<TimePickerModalProps> = ({
    visible,
    onClose,
    onConfirm,
}) => {
    const [hours, setHours] = useState('07');
    const [minutes, setMinutes] = useState('30');

    const hoursRef = useRef<FlatList>(null);
    const minutesRef = useRef<FlatList>(null);

    const centerIndex = Math.floor(REPEAT_COUNT / 2);

    const scrollToInitial = () => {
        const hourIndex = centerIndex * 24 + parseInt(hours, 10);
        const minuteIndex = centerIndex * 60 + parseInt(minutes, 10);

        hoursRef.current?.scrollToIndex({ index: hourIndex, animated: false });
        minutesRef.current?.scrollToIndex({ index: minuteIndex, animated: false });
    };

    useEffect(() => {
        if (visible) {
            setTimeout(scrollToInitial, 0);
        }
    }, [visible]);

    const handleScroll = (
        list: 'hours' | 'minutes',
        e: NativeSyntheticEvent<NativeScrollEvent>,
        max: number
    ) => {
        const y = e.nativeEvent.contentOffset.y;
        const index = Math.round(y / ITEM_HEIGHT) % max;
        const value = index.toString().padStart(2, '0');

        list === 'hours' ? setHours(value) : setMinutes(value);
    };

    const renderItem = (
        value: string,
        currentValue: string
    ) => {
        const isActive = value === currentValue;

        return (
            <View style={styles.pickerItem}>
                <Text style={[styles.pickerItemText, isActive && styles.pickerItemTextSelected]}>
                    {value}
                </Text>
            </View>
        );
    };

    const handleConfirm = () => {
        onConfirm(hours, minutes);
    };

    return (
        <CustomModal
            visible={visible}
            onClose={onClose}
            onConfirm={handleConfirm}
            defaultButtons={true}
        >
            <View style={styles.timeContainer}>
                <View style={styles.staticSelector} />

                <View style={styles.pickerContainer}>
                    <FlatList
                        ref={hoursRef}
                        data={hoursList}
                        keyExtractor={(item, index) => `h-${index}`}
                        getItemLayout={(_, index) => ({
                            length: ITEM_HEIGHT,
                            offset: ITEM_HEIGHT * index,
                            index,
                        })}
                        showsVerticalScrollIndicator={false}
                        onMomentumScrollEnd={(e) => handleScroll('hours', e, 24)}
                        snapToInterval={ITEM_HEIGHT}
                        decelerationRate="fast"
                        style={styles.picker}
                        contentContainerStyle={styles.pickerContent}
                        renderItem={({ item }) => renderItem(item, hours)}
                    />
                    <Text style={styles.timeLabel}>h</Text>
                </View>

                <View style={styles.pickerContainer}>
                    <FlatList
                        ref={minutesRef}
                        data={minutesList}
                        keyExtractor={(item, index) => `m-${index}`}
                        getItemLayout={(_, index) => ({
                            length: ITEM_HEIGHT,
                            offset: ITEM_HEIGHT * index,
                            index,
                        })}
                        showsVerticalScrollIndicator={false}
                        onMomentumScrollEnd={(e) => handleScroll('minutes', e, 60)}
                        snapToInterval={ITEM_HEIGHT}
                        decelerationRate="fast"
                        style={styles.picker}
                        contentContainerStyle={styles.pickerContent}
                        renderItem={({ item }) => renderItem(item, minutes)}
                    />
                    <Text style={styles.timeLabel}>m</Text>
                </View>
            </View>
        </CustomModal>
    );
};

const styles = StyleSheet.create({
    timeContainer: {
        flexDirection: 'row',
        justifyContent: 'center',
        alignItems: 'center',
        marginBottom: 15,
        height: ITEM_HEIGHT * VISIBLE_ITEMS,
        position: 'relative',
    },
    pickerContainer: {
        flexDirection: 'row',
        alignItems: 'center',
        marginHorizontal: 15,
        height: ITEM_HEIGHT * VISIBLE_ITEMS,
        overflow: 'hidden',
    },
    picker: {
        height: ITEM_HEIGHT * VISIBLE_ITEMS,
        width: 60,
    },
    pickerContent: {
        paddingTop: ITEM_HEIGHT * 2,
        paddingBottom: ITEM_HEIGHT * 2,
    },
    pickerItem: {
        height: ITEM_HEIGHT,
        justifyContent: 'center',
        alignItems: 'center',
    },
    pickerItemText: {
        fontSize: 18,
        color: '#8A8BB3',
        fontFamily: 'HindSiliguri-400',
    },
    pickerItemTextSelected: {
        fontSize: 20,
        fontWeight: 'bold',
        color: '#4A4AFF',
        fontFamily: 'HindSiliguri-600',
    },
    timeLabel: {
        fontSize: 20,
        color: '#4A4AFF',
        fontFamily: 'HindSiliguri-700',
    },
    staticSelector: {
        position: 'absolute',
        top: ITEM_HEIGHT * Math.floor(VISIBLE_ITEMS / 2),
        left: 0,
        right: 0,
        height: ITEM_HEIGHT,
        backgroundColor: '#F0F0F0',
        borderRadius: 8,
        zIndex: 0,
    },
});

export { TimePickerModal };

Ответы (0 шт):