reactjsreact-nativeanimationreact-animated

react native infinite loop of bounce animations on a list of Views?


suppose I have an array of data which in this context is store names. I need to animate them in a way like first one store enters about 10px from above to the box with scale of 0.5 and after a short delay it continues the get a bit of center and get bigger sanctimoniously (parallel) and after another delay it goes down of the box and desperate. like below:

enter image description here

so far I achieve this. and what I need is the next store to come after this one and wait for getting bigger, something like below:

enter image description here

the little above waits for the big one to drop and then start the same animation.

here is my code so far:

const TrackOrder = () => {
  const [listData, setListData] = useState([
    {
      id: 1,
      storeName: 'papa jones besiktas',
    },
    {
      id: 2,
      storeName: 'levent store',
    },
     {
       id: 3,
       storeName: 'sariyer store',
     },
  ]);
  const {colors} = useTheme();
  // let fadeAni = useRef(new Animated.Value(0.2)).current;
  let bounceLittleItem = useRef(new Animated.Value(-80)).current;
  let bounceBigItem = useRef(new Animated.Value(-100)).current;

  let scaleUp = useRef(new Animated.Value(0.5)).current;

  const styles = useMemo(
    () =>
      StyleSheet.create({
        mainContainer: {
          backgroundColor: colors.CONTRAST_PRIMARY,
          height: 100,
          borderRadius: 30,
          marginHorizontal: `${(100 - GUTTERS.SCREEN_WIDTH_IN_NUMBER) / 2}%`,
          flexDirection: 'column',
          alignItems: 'flex-start',
          justifyContent: 'center',
          paddingHorizontal: 15,
          marginTop: 25,
          overflow: 'hidden',
        },
        orderContainer: {
          flexDirection: 'row',
          alignItems: 'center',
          justifyContent: 'flex-start',
        },
        storeImage: {
          width: 45,
          height: 45,
          backgroundColor: 'yellow',
          borderRadius: 40,
          marginRight: 20,
        },
        orderStatusText: {
          color: '#fff',
          fontFamily: 'BalooPaaji2-SemiBold',
        },
        storeContainer: {
          backgroundColor: '#fff',
          paddingHorizontal: 10,
          paddingVertical: 5,
          borderRadius: 20,
        },
        storeOneBeforeLastImage: {
          width: 25,
          height: 25,
          backgroundColor: 'yellow',
          borderRadius: 40,
          // marginRight: 20,
          opacity: 0.5,
        },
      }),
    [colors],
  );


  useEffect(() => {
    const inter = setInterval(() => {
      let copyArr = [...listData];
      let last = copyArr.pop();
      copyArr.unshift(last);
      setListData(copyArr);
    }, 6000);

    return () => clearInterval(inter);
  }, [listData]);

  useEffect(() => {
    Animated.timing(bounceBigItem, {
      toValue: -30,
      duration: 2000,
      useNativeDriver: true,
    });
  }, [bounceBigItem]);

  const runAnimation = useCallback(() => {
    Animated.sequence([
      Animated.timing(bounceLittleItem, {
        toValue: -30,
        duration: 2000,
        useNativeDriver: true,
      }),
      Animated.parallel([
        Animated.timing(bounceLittleItem, {
          toValue: 20,
          duration: 1000,
          useNativeDriver: true,
        }),
        Animated.timing(scaleUp, {
          toValue: 1,
          duration: 1000,
          useNativeDriver: true,
        }),
      ]),
      Animated.delay(2000),
      Animated.timing(bounceLittleItem, {
        toValue: 100,
        duration: 1000,
        useNativeDriver: true,
      }),
    ]).start(() => {
      bounceLittleItem.setValue(-80);
      scaleUp.setValue(0.5);
      runAnimation();
    });
  }, [bounceLittleItem, scaleUp]);

  useEffect(() => runAnimation(), [runAnimation]);

  const renderViewItem = useMemo(() => {
    if (listData?.length === 0) return;
    return listData.map((el, i) => {
      return (
        <Animated.View
          key={el.id}
          style={[
            styles.orderContainer,
            i === 0
              ? {
                  transform: [{translateY: bounceLittleItem}, {scale: scaleUp}],
                }
              : {
                  transform: [{translateY: bounceBigItem}, {scale: 0.5}],
                },
          ]}
        >
          <View style={[styles.storeImage]} />
          <Text style={styles.orderStatusText}>{el.storeName}</Text>
        </Animated.View>
      );
    });
  }, [bounceBigItem, bounceLittleItem, listData, scaleUp, styles.orderContainer, styles.orderStatusText, styles.storeImage]);

  return <View style={styles.mainContainer}>{renderViewItem}</View>;
};

so, how can I achieve my desired animation which once again is first come down and as soon as getting bigger next store come down and step on first store footprints? if you have any idea I really appreciate.


Solution

  • I devised a solution that works by firing the animation for every item and applying a delay according to the item's position in the list (index). The changes consist in piling up all items at the top with position: absolute, and each one comes down with a delay. Additionally, I used Animated.loop function since it seems to work more consistently.

    import React, {useRef, useEffect, useCallback} from 'react';
    import {StyleSheet, View, Text, Animated} from 'react-native';
    
    const Item = ({children, index, len}) => {
      let bounceLittleItem = useRef(new Animated.Value(-80)).current;
      let scaleUp = useRef(new Animated.Value(0.5)).current;
    
      const runAnimation = useCallback(
        delay => {
          Animated.sequence([
            Animated.delay(delay),
            Animated.loop(
              Animated.sequence([
                Animated.timing(bounceLittleItem, {
                  toValue: -30,
                  duration: 2000,
                  useNativeDriver: true,
                }),
                Animated.parallel([
                  Animated.timing(bounceLittleItem, {
                    toValue: 20,
                    duration: 1000,
                    useNativeDriver: true,
                  }),
                  Animated.timing(scaleUp, {
                    toValue: 1,
                    duration: 1000,
                    useNativeDriver: true,
                  }),
                ]),
                Animated.delay(1000),
                Animated.timing(bounceLittleItem, {
                  toValue: 100,
                  duration: 1000,
                  useNativeDriver: true,
                }),
                Animated.delay((2 * len - 5) * 1000),
              ]),
            ),
          ]).start();
        },
        [bounceLittleItem, scaleUp, len],
      );
    
      useEffect(() => {
        console.log(`running animation ${index}`);
        runAnimation(index * 2000);
      }, [index, runAnimation]);
    
      return (
        <Animated.View
          style={[
            styles.orderContainer,
            {
              transform: [{translateY: bounceLittleItem}, {scale: scaleUp}],
            },
          ]}>
          {children}
        </Animated.View>
      );
    };
    
    const App = () => {
      const listData = [
        {
          id: 1,
          storeName: 'papa jones besiktas',
        },
        {
          id: 2,
          storeName: 'levent store',
        },
        {
          id: 3,
          storeName: 'sariyer store',
        },
      ];
    
      return (
        <View style={styles.mainContainer}>
          {listData.map(({id, storeName}, index) => (
            <Item key={id} index={index} len={listData.length}>
              <View style={[styles.storeImage]} />
              <Text style={styles.orderStatusText}>{storeName}</Text>
            </Item>
          ))}
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      mainContainer: {
        backgroundColor: 'black',
        height: 100,
        borderRadius: 30,
        flexDirection: 'column',
        alignItems: 'flex-start',
        justifyContent: 'center',
        paddingHorizontal: 15,
        marginTop: 25,
        overflow: 'hidden',
      },
      orderContainer: {
        width: '100%',
        position: 'absolute',
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'flex-start',
      },
      storeImage: {
        width: 45,
        height: 45,
        backgroundColor: 'yellow',
        borderRadius: 40,
        marginRight: 20,
      },
      orderStatusText: {
        color: '#fff',
        fontFamily: 'BalooPaaji2-SemiBold',
      },
      storeContainer: {
        backgroundColor: '#fff',
        paddingHorizontal: 10,
        paddingVertical: 5,
        borderRadius: 20,
      },
      storeOneBeforeLastImage: {
        width: 25,
        height: 25,
        backgroundColor: 'yellow',
        borderRadius: 40,
        // marginRight: 20,
        opacity: 0.5,
      },
    });
    export default App;
    

    https://snack.expo.dev/@diedu89/animation-loop

    Be aware values are tailored to the duration of the animations and how close you want to run them in "parallel". I came up with the formula (2 * len - 5) for the delay of the next loop by tabulating a set of points and using an online tool to get it

    For instance, with a timeline looking like this for the animation during 5000 ms and each one fired with a 2000 difference

    start finish
    0 5000
    2000 7000
    4000 9000
    6000 11000
    8000 13000

    I could determine that for a array with length of 3 I'd need 1000 of delay, for 4 3000, for 5 5000, and so on

    length delay
    3 1000
    4 3000
    5 5000
    6 7000
    7 9000