Flatlist auto select image carousel flickering in iOS, working in Android

121 Views Asked by At

I have created an image carousel wherein as you scroll, image gets auto selected. The selected image should expand and push the images on left and right aside.

Consumer code

import { View, Animated } from 'react-native';
import React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import { AutoSelectScrollList } from './AutoSelectScrollList';
import { IListData } from './AutoSelectScrollList/types';

export default function AutoSelectScrollView() {
    const collectionStyles = (theme: Theme) =>
        StyleSheet.create({
            singleCollections: {
                width: 80,
                marginVertical: 20,
            },
            collectionImage: {
                width: '100%',
                borderRadius: 12,
            },
            collectionText: {
                marginTop: 8,
                fontSize: 12,
                fontWeight: '400',
                color: 'grey,
                textAlign: 'center',
            },
            contentContainer: {
                paddingHorizontal: 16,
            },
            colorSelected: {
                color: 'red',
            },
            imageSelected: {
                width: 80,
            },
            imageSelected2: {
                width: 116,
            },
        });

    const testData = [
        {
            image: require('../../../common/assets/images/1.png'),
            title: 'Friendly Classic0',
            type: '',
            id: 0,
        },
        {
            image: require('../../../common/assets/images/2.png'),
            title: 'Southeast asia1',
            type: '',
            id: 1,
        },
        {
            image: require('../../../common/assets/images/3.png'),
            title: 'Camping Adventure2',
            type: '',
            id: 2,
        },
        {
            image: require('../../../common/assets/images/4.png'),
            title: 'Off the grid3',
            type: '',
            id: 3,
        },
        {
            image: require('../../../common/assets/images/1.png'),
            title: 'Camping Adventure4',
            type: '',
            id: 4,
        },
        {
            image: require('../../../common/assets/images/2.png'),
            title: 'Off the grid5',
            type: '',
            id: 5,
        },
        {
            image: require('../../../common/assets/images/3.png'),
            title: 'Camping Adventure6',
            type: '',
            id: 6,
        },
        {
            image: require('../../../common/assets/images/4.png'),
            title: 'Off the grid7',
            type: '',
            id: 7,
        },
    ];

    const [{ style: themedStyle }] = useTheme(collectionStyles);

    const [selectedId, setSelectedId] = useState(4);
    const animatedValue = new Animated.Value(0);
    useEffect(() => {
        Animated.timing(animatedValue, {
            toValue: 1, // The final value
            duration: 1000, // Animation duration in milliseconds
            delay: 500, // Delay the animation start by 500 milliseconds
            useNativeDriver: false, // Set this to true whenever possible for better performance
        }).start();
    }, [selectedId]);

    const translateXToRight = animatedValue.interpolate({
        inputRange: [0, 1],
        outputRange: [0, 10], // Adjust as needed
    });

    const translateXToLeft = animatedValue.interpolate({
        inputRange: [0, 1],
        outputRange: [0, -10], // Adjust as needed
    });

    const scaleX = animatedValue.interpolate({
        inputRange: [0, 1],
        outputRange: [1, 1.45], // Adjust as needed
    });

    const width = animatedValue.interpolate({
        inputRange: [0, 1],
        outputRange: [80, 116], // Adjust as needed
    });

    const renderFn = (
        item: IListData,
        enableAutoSelectMode: boolean,
        selectedIndex: number,
    ) => {
        setSelectedId(selectedIndex);
        return (
            <Animated.View
                style={[
                    themedStyle.singleCollections,
                    selectedIndex < item.id &&
                        !enableAutoSelectMode && {
                            transform: [{ translateX: translateXToRight }],
                        },
                    selectedIndex > item.id &&
                        !enableAutoSelectMode && {
                            transform: [{ translateX: translateXToLeft }],
                        },
                ]}
            >
                <View
                    style={[
                        {
                            alignItems: 'center',
                            justifyContent: 'center',
                        },
                    ]}
                >
                    <Animated.Image
                        source={item.image}
                        style={[
                            themedStyle.collectionImage,
                            selectedIndex === item.id &&
                                !enableAutoSelectMode && {
                                    transform: [{ scaleX }],
                                },
                        ]}
                    />
                    <Animated.Text
                        numberOfLines={2}
                        style={[
                            themedStyle.collectionText,
                            selectedIndex === item.id &&
                                themedStyle.colorSelected,
                            selectedIndex === item.id &&
                                !enableAutoSelectMode && { width },
                        ]}
                    >
                        {item.title}
                    </Animated.Text>
                </View>
            </Animated.View>
        );
    };

    return (
        <AutoSelectScrollList
            initialSelectedIndex={4}
            listData={testData}
            renderListData={renderFn}
            onPress={item => {
                console.log(`Pressed ${item}`);
            }}
            peekOffset={60}
            contentContainerStyle={themedStyle.contentContainer}
        />
    );
}

Generic component to create the auto select carousel


import React from 'react';
import {
    FlatList,
    NativeSyntheticEvent,
    NativeScrollEvent,
    LayoutChangeEvent,
    View,
    Pressable
} from 'react-native';
import {
    IAutoSelectScrollList,
    IListData,
    IState,
    ITabMeasurements,
    ITabsLayoutRectangle,
} from './types';

const SCREEN_WIDTH = 400
export class AutoSelectScrollList extends React.Component<
    IAutoSelectScrollList,
    IState
> {
    state: IState = {
        enableAutoSelectMode: false,
        startViewPort: 0,
        endViewPort: SCREEN_WIDTH,
        selectedIndex: 0,
        isScrollOngoing: false,
    };

    static defaultProps = {
        initialSelectedIndex: 0,
        autoScrollDuration: 300,
        peekOffset: 10,
    };

    private flatListRef: React.RefObject<FlatList> = React.createRef();
    private _tabsMeasurements: ITabsLayoutRectangle = {};

    componentDidMount() {
        const { initialSelectedIndex, listData, autoScrollDuration } =
            this.props;
        if (
            initialSelectedIndex !== undefined &&
            initialSelectedIndex >= 0 &&
            initialSelectedIndex < listData.length
        ) {
            this.setState(prevState => ({
                ...prevState,
                selectedIndex: initialSelectedIndex,
            }));
            setTimeout(() => {
                this.flatListRef.current?.scrollToIndex({
                    animated: true,
                    index: initialSelectedIndex,
                });
            }, autoScrollDuration);
        }
    }

    onTabLayout = (key: number) => (ev: LayoutChangeEvent) => {
        const { x, width, height } = ev.nativeEvent.layout;
        this._tabsMeasurements[key] = {
            left: x,
            right: x + width,
            width,
            height,
        };
    };

    getScrollAmount = (position: number, measurements: ITabMeasurements) => {
        const { listData, peekOffset } = this.props;
        const { startViewPort, endViewPort } = this.state;
        const lastIndexSupported = listData.length - 1;
        if (position >= lastIndexSupported) {
            return SCREEN_WIDTH;
        }
        const tabWidth = measurements.width;
        const tabOffset = measurements.left;
        const newScrollX = tabOffset + tabWidth;
        const diff = SCREEN_WIDTH - newScrollX;
        if (startViewPort <= tabOffset && newScrollX <= endViewPort) {
            return null;
        }
        if (diff < 0) {
            return -diff + peekOffset;
        } else {
            return tabOffset - peekOffset;
        }
    };

    scrollToOffset = (offset: number) => {
        const { autoScrollDuration } = this.props;
        if (this.flatListRef.current) {
            setTimeout(() => {
                this.flatListRef.current?.scrollToOffset({
                    animated: true,
                    offset,
                });
            }, autoScrollDuration);
        }
    };

    onPillPress = (item: IListData) => {
        const getScrollXPos = this.getScrollAmount(
            item.id,
            this._tabsMeasurements[item.id],
        );
        getScrollXPos && this.scrollToOffset(getScrollXPos);
        this.setState(prevState => ({
            ...prevState,
            selectedIndex: item.id,
        }));
        this.props.onPress && this.props.onPress(item);
    };

    renderData = (item: IListData, id: number) => {
        const { enableAutoSelectMode, selectedIndex } = this.state;
        const { renderListData } = this.props;
        return (
            <View
                onLayout={this.onTabLayout(id)}
                key={id}
                style={{ marginHorizontal: 8 }}
            >
                <Pressable onPress={() => this.onPillPress(item)}>
                    {renderListData(item, enableAutoSelectMode, selectedIndex)}
                </Pressable>
            </View>
        );
    };

    handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
        const { enableAutoSelectMode, startViewPort, selectedIndex } =
            this.state;
        const { listData } = this.props;
        const { contentOffset, contentSize } = event.nativeEvent;
        const contentOffsetX = contentOffset.x;
        const avgPerItemLength = SCREEN_WIDTH / listData.length;
        const isRightScroll = contentOffsetX > startViewPort ? true : false;
        this.setState(prevState => ({
            ...prevState,
            isScrollOngoing: true,
        }));
        if (enableAutoSelectMode) {
            if (contentOffsetX < 10) {
                this.setState(prevState => ({
                    ...prevState,
                    selectedIndex: 0,
                    startViewPort: contentOffsetX,
                    endViewPort: contentOffsetX + SCREEN_WIDTH,
                }));
            } else if (contentOffsetX > contentSize.width - 10) {
                this.setState(prevState => ({
                    ...prevState,
                    selectedIndex: listData.length - 1,
                    startViewPort: contentOffsetX,
                    endViewPort: contentOffsetX + SCREEN_WIDTH,
                }));
            } else {
                let newIndex = Math.floor(contentOffsetX / avgPerItemLength);
                if (newIndex >= listData.length) {
                    newIndex = listData.length - 1;
                } else if (newIndex < 0) {
                    newIndex = 0;
                }
                if (isRightScroll && newIndex > selectedIndex) {
                    this.setState(prevState => ({
                        ...prevState,
                        selectedIndex: newIndex,
                        startViewPort: contentOffsetX,
                        endViewPort: contentOffsetX + SCREEN_WIDTH,
                    }));
                } else if (!isRightScroll && newIndex < selectedIndex) {
                    this.setState(prevState => ({
                        ...prevState,
                        selectedIndex: newIndex,
                        startViewPort: contentOffsetX,
                        endViewPort: contentOffsetX + SCREEN_WIDTH,
                    }));
                }
            }
        }
        this.setState(prevState => ({
            ...prevState,
            startViewPort: contentOffsetX,
            endViewPort: contentOffsetX + SCREEN_WIDTH,
        }));
    };

    handleScrollBegin = () => {
        this.setState(prevState => ({
            ...prevState,
            enableAutoSelectMode: true,
        }));
    };

    handleOnMomentumScrollEnd = () => {
        /**
         * There is a bug which calls onMomentumScrollEnd 3 times.
         * Using isScrollOngoing flag for this
         * https://github.com/facebook/react-native/pull/32433
         */
        console.log('hit');

        this.state.isScrollOngoing &&
            this.onPillPress(this.props.listData[this.state.selectedIndex]);

        this.setState(prevState => ({
            ...prevState,
            enableAutoSelectMode: false,
            isScrollOngoing: false,
        }));
    };

    render() {
        const {
            listData,
            contentContainerStyle = null,
            itemSeparatorView = null,
        } = this.props;
        return (
            /**
             * We need to use CellRendererComponent instead of renderItem
             * renderItem wraps each data inside a view.
             * Due to this we will not be able to dimensions of each item in list
             */
            <FlatList
                ref={this.flatListRef}
                data={listData}
                contentContainerStyle={contentContainerStyle}
                ItemSeparatorComponent={() => itemSeparatorView}
                alwaysBounceHorizontal={false}
                keyExtractor={item => item.id.toString()}
                CellRendererComponent={({ item }) =>
                    this.renderData(item, item.id)
                }
                maintainVisibleContentPosition={{
                    minIndexForVisible: this.state.selectedIndex,
                }}
                horizontal
                bounces={true}
                onScroll={this.handleScroll}
                onScrollBeginDrag={this.handleScrollBegin}
                onMomentumScrollEnd={this.handleOnMomentumScrollEnd}
                getItemLayout={(data, index) => ({
                    length: 200,
                    offset: 200,
                    index,
                })}
            />
        );
    }
}

The code works perfectly in android but in ios there are constant flickers! On scroll the image is getting higlighted correctly, but all images are getting re-rendered. This is happening only in iOS and not in Android which is causing the flicker view

0

There are 0 best solutions below