react-nativeexporeact-native-reanimated-v2

Random Animation within map function - Expo - Reanimated2


I have a screen with four animated Views. They are rendered by a map function. When the screen is mounted a scale animation is triggered for once. After that, i'm trying to achieve to randomly animate (scale simple animation) only one View every 4 seconds. I can't find the way to get the reference of each View and only animate one randomly. Here is my code:

import { useRef, useEffect } from 'react'
import { View, StyleSheet, TouchableOpacity } from 'react-native'
import Animated, { useAnimatedStyle, useSharedValue, withTiming, withSpring, withRepeat } from 'react-native-reanimated'

const SIZE = 65.0

const RandomZoomInScreen = ({ navigation }) => {

  const scale = useSharedValue(0)
  const bounce = useSharedValue(1)
  const itemEls = useRef(new Array())

  const reanimatedStyle = useAnimatedStyle(() => {
    return {
      transform: [{ scale: scale.value }]
    }
  })

  const bounceStyle = useAnimatedStyle(() => {
    return {
      transform: [{ scale: bounce.value }]
    }
  })

  useEffect(() => {
    scale.value = withSpring(1, {stiffness:200})
    return () => {
      scale.value = 0
    }
  }, [])

  useEffect(() => {
    const intervalId = setInterval(() => {  //assign interval to a variable to clear it.
     'worklet'
      bounce.value = withRepeat(withTiming(1.3),2,true)
    }, 4000)
  
    return () => clearInterval(intervalId)
  }, [])

  return (
    <View style={styles.container}>
      {[1, 2, 3, 4].map((square, index) => {
        return <TouchableOpacity
            activeOpacity={0.7}
            key={index}
          >
          <Animated.View 
          
          ref={(element) => itemEls.current.push(element)} style={[styles.square, reanimatedStyle, bounceStyle]} />
        </TouchableOpacity>
      })}

    </View>
  )

}

export default RandomZoomInScreen

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'space-evenly',
    alignItems: 'center'
  },
  square: {
    width: SIZE * 2,
    height: SIZE,
    backgroundColor: 'green',
  }
})

Solution

  • You want to animate a single box every 4 seconds, right?

    If you want to animate a single box, I think the best way is to create individual shared values for each box, by either creating a Box component, or creating an array of shared values (or something like that), so that you can change each value independently.

    Here I refactored your code, creating a Box component, and creating a scale shared value inside the component:

    import { useEffect, useState } from "react";
    import { View, StyleSheet, TouchableOpacity } from "react-native";
    import Animated, {
      useAnimatedStyle,
      useSharedValue,
      withSpring,
      withSequence,
      withTiming,
    } from "react-native-reanimated";
    
    const SIZE = 65.0;
    
    const Box = ({ shouldAnimate }) => {
      const scale = useSharedValue(0);
      const reanimatedStyle = useAnimatedStyle(() => {
        return {
          transform: [{ scale: scale.value }],
        };
      });
    
      useEffect(() => {
        scale.value = withSpring(1, { stiffness: 200 });
        return () => {
          scale.value = 0;
        };
      }, []);
    
      useEffect(() => {
        if (shouldAnimate) {
          scale.value = withSequence(withTiming(1.3), withTiming(1));
        }
      }, [shouldAnimate]);
    
      return (
        <TouchableOpacity activeOpacity={0.7}>
          <Animated.View style={[styles.square, reanimatedStyle]} />
        </TouchableOpacity>
      );
    };
    
    const RandomZoomInScreen = ({ navigation }) => {
      const [selectedBox, setSelectedBox] = useState(-1);
    
      useEffect(() => {
        const intervalId = setInterval(() => {
          // you want to select a number between 0 and 3 (the indeces of the boxes) every 4 seconds
          const nextBox = Math.floor(Math.random() * 4);
    
          // to garantee the new value will always be different from previous one...
          // we can sum 1 and apply a mod 4 (so the result is always between 0 and 3)
          setSelectedBox((previousBox) =>
            previousBox === nextBox ? (nextBox + 1) % 4 : nextBox
          );
        }, 4000);
    
        return () => clearInterval(intervalId);
      }, []);
    
      return (
        <View style={styles.container}>
          {[1, 2, 3, 4].map((square, index) => {
            // we should animate when the selected box is equal to the index
            return <Box key={square} shouldAnimate={index === selectedBox} />;
          })}
        </View>
      );
    };
    
    export default RandomZoomInScreen;
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: "space-evenly",
        alignItems: "center",
      },
      square: {
        width: SIZE * 2,
        height: SIZE,
        backgroundColor: "green",
      },
    });
    

    Keep in mind that there are probably other approaches to achieving this.