dohooo / react-native-reanimated-carousel

🎠 React Native swiper/carousel component, fully implemented using reanimated v2, support to iOS/Android/Web. (Swiper/Carousel)
https://react-native-reanimated-carousel.vercel.app
MIT License
2.71k stars 313 forks source link

Flashing items when appending data #347

Open kierancrown opened 1 year ago

kierancrown commented 1 year ago

Describe the bug I'm attempting to to create a component where the user can scroll back through data and as they do so more data loads in and gets appended to the data array. However when I do this I noticed that the items change order. I'm looking for a way to have the new items load in but keep the current index. I attempted this by calling the scrollTo function with animation set to false just after adding the new data. However this causes the component to render twice and the user see's a flash during this process. I'm wondering if there is a way to achieve this without flashing.

Here is a basic example of the code I'm using:

import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {
  Button,
  Dimensions,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import {TAnimationStyle} from 'react-native-reanimated-carousel/src/layouts/BaseLayout';
import {interpolate, useSharedValue, withTiming} from 'react-native-reanimated';
import Carousel, {ICarouselInstance} from 'react-native-reanimated-carousel';
import {generateRandomBarHeights} from '../utils';
import {Freeze} from 'react-freeze';
import BarChart from './BarChart';
import {triggerHaptic} from '../utils/haptics';

const HEIGHT = 300;

const generateBarChart = (numberOfCharts = 1, barColor?: string) => {
  let elements: JSX.Element[] = [];
  for (let i = 0; i < numberOfCharts; i++) {
    const barHeights = generateRandomBarHeights(20, HEIGHT - 95);
    elements[i] = (
      <BarChart height={HEIGHT} barValues={barHeights} barColor={barColor} />
    );
  }
  return elements;
};

const width = Dimensions.get('window').width;
function Swiper(): JSX.Element {
  const carouselRef = React.useRef<ICarouselInstance>(null);

  const initialData = useMemo(() => generateBarChart(5), []);
  const [data, setData] = useState(initialData);

  const [autoAdd, setAutoAdd] = useState(false);

  const [index, setIndex] = useState(0);
  const [indexText, setIndexText] = useState(`${index + 1} / ${data.length}`);

  const [shouldSuspendRendering, setShouldSuspendRendering] = useState(false);

  const pressAnim = useSharedValue<number>(0);
  const animationStyle: TAnimationStyle = React.useCallback((value: number) => {
    'worklet';

    const zIndex = interpolate(value, [-1, 0, 1], [-1000, 0, 1000]);
    const translateX = interpolate(value, [-1, 0, 1], [-width, 0, width]);
    const scale = interpolate(value, [-1, 0, 1], [0.8, 1, 0.8]);

    return {
      transform: [{translateX}, {scale}],
      zIndex,
    };
  }, []);

  const handleNavigation = (direction: 'prev' | 'next') => {
    if (carouselRef.current) {
      if (direction === 'prev') {
        carouselRef.current.prev();
      } else {
        carouselRef.current.next();
      }
    }
  };

  const updateIndexText = useCallback(
    (i?: number) => {
      const newIndex = i ? i : carouselRef.current?.getCurrentIndex() || index;
      setIndexText(`${newIndex + 1} / ${data.length}`);
    },
    [data.length, index],
  );

  useEffect(() => {
    updateIndexText();
  }, [data, index, updateIndexText]);

  const addToStart = useCallback(() => {
    // Take current index
    // setShouldSuspendRendering(true);
    const currentIndex = carouselRef.current?.getCurrentIndex() || 0;
    setData(generateBarChart(1, '#ff7675').concat(data));
    // Set index to the old index
    if (carouselRef.current) {
      carouselRef.current.scrollTo({
        animated: false,
        index: currentIndex + 1,
        count: data.length,
        onFinished() {
          setShouldSuspendRendering(false);
          updateIndexText();
        },
      });
    }
  }, [data, updateIndexText]);

  const addToEnd = () => {
    setData([...data, ...generateBarChart(1, '#00cec9')]);
  };

  const resetData = () => {
    setData(initialData);
    if (carouselRef.current) {
      carouselRef.current.scrollTo({
        animated: false,
        index: 0,
        count: data.length,
      });
    }
    updateIndexText();
  };

  const toggleAutoAdd = () => {
    setAutoAdd(!autoAdd);
  };

  const onSnapToItem = (i: number) => {
    triggerHaptic('impactLight');
    setIndex(i);
    updateIndexText(i + 1);
    if (autoAdd && i < 1) {
      addToStart();
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.textContainer}>
        <Text style={styles.text}>{indexText}</Text>
      </View>
      <View style={styles.buttonContainer}>
        <Button
          title={!autoAdd ? 'Enable Auto Add' : 'Disable Auto Add'}
          color={'#0984e3'}
          onPress={toggleAutoAdd}
        />
      </View>
      <View style={styles.buttonContainer}>
        <Button title="Add to start" color={'#ff7675'} onPress={addToStart} />
        <Button title="Reset" color={'#0984e3'} onPress={resetData} />
        <Button title="Add to end" color={'#00cec9'} onPress={addToEnd} />
      </View>
      <Freeze freeze={shouldSuspendRendering}>
        <Carousel
          ref={carouselRef}
          loop={false}
          width={width - 40}
          height={HEIGHT}
          data={data}
          customAnimation={animationStyle}
          scrollAnimationDuration={250}
          onSnapToItem={onSnapToItem}
          renderItem={({index: i}) => data[i]}
          onScrollBegin={() => {
            pressAnim.value = withTiming(1);
          }}
          onScrollEnd={() => {
            pressAnim.value = withTiming(0);
          }}
        />
      </Freeze>
      <View style={styles.buttonContainer}>
        <Button
          title="Previous"
          color={'#0984e3'}
          onPress={() => handleNavigation('prev')}
        />
        <Button
          title="Next"
          color={'#0984e3'}
          onPress={() => handleNavigation('next')}
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#fff',
    borderRadius: width / 30,
  },
  textContainer: {
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 10,
  },
  buttonContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginVertical: 10,
  },
  text: {
    fontSize: 20,
    fontWeight: 'bold',
  },
});

export default Swiper;

To Reproduce Steps to reproduce the behavior:

  1. Create a new react-native-carousel project
  2. Copy the example code I have given above
  3. Press add to start
  4. See error

Expected behavior The component to not flash when adding data and updating the index (some way to freeze the updates...)

Screenshots Simulator Screen Recording - iPhone 14 - 2023-01-17 at 18 54 26

Versions (please complete the following information):

Smartphone (please complete the following information):

RobertMrowiec commented 1 year ago

I'm looking for this solution too 😔

mustafakameldev commented 1 year ago

I hope this solution could help you,

In my case, This issue was when I added data that could be updated after the first render, The solution was I create a new component that home this data and wrapped it on horizontal scrollView and It works