Home > OS >  ReactNative - Animate multiple views size based on condition
ReactNative - Animate multiple views size based on condition

Time:12-22

I have a view full of red circles.

I would like to animate their size/borderRadius based on which circle is highlighted (biggerCircleIndex)

The code looks like this now:

import React from 'react';

const Circles = props => {

  const myCircles = [0, 1, 2, 3, 4, 5];
  const [biggerCircleIndex, setBiggerCircleIndex] = useState(2);
 
  return (
    <View>
      {myCircles.map((index) => {
         return (
            <Animated.View
              key={index}
              style={[
                {backgroundColor: 'red'},
                (biggerCircleIndex == index)
                  ? {width: 100, height: 100, borderRadius: 50}
                  : {width: 50, height: 50, borderRadius: 25},
              ]}>
            </Animated.View>
          );
      })}
    </View>
  );
};
export {Circles};

Tried using animation for the size but failed: the circles aren't animated when I update the biggerCircleIndex

I think I should create an array of animations, but I really don't know how.

Can someone help me out?

Here's the code snippet how I've been trying:

import React from 'react';

const Circles = props => {

  const myCircles = [0, 1, 2, 3, 4, 5];
  const [biggerCircleIndex, setBiggerCircleIndex] = useState(2);

  const inputRange = [0, 100];
  const outputRange = [50, 100];

  const animation = React.useRef(new Animated.Value(0)).current
  const animatedSize = animation.interpolate({inputRange, outputRange});

  const animationReverse = React.useRef(new Animated.Value(100)).current
  const animatedSizeReverse = animationReverse.interpolate({inputRange, outputRange});

  React.useEffect(() => {
      Animated.timing(
          animation,
          {
            toValue: 100,
            duration: 500,
            useNativeDriver: false,
          }
      ).start();
  }, [animation])
  React.useEffect(() => {
      Animated.timing(
          animationReverse,
          {
            toValue: 0,
            duration: 500,
            useNativeDriver: false,
          }
      ).start();
  }, [animationReverse])

  return (
    <View>
      {myCircles.map((index) => {
         return (
            <Animated.View
              key={index}
              style={[
                {backgroundColor: 'red'},
                (biggerCircleIndex == index)
                  ? {width: animatedSize, height: animatedSize, borderRadius: 50}
                  : {width: animatedSizeReverse, height: animatedSizeReverse, borderRadius: 25},
              ]}>
            </Animated.View>
          );
      })}
    </View>
  );
};
export {Circles};

CodePudding user response:

For achieving this, I think one of the best approaches is creating an independent Circle component. That way, each circle would have its own Animated.Value.

Also, since you only want to animate the radius (height and width are the same value) and borderRadius (which is half the radius) we'd only need two Animated.Value's.

We'd also need something to trigger the animation. In your example, I think the animations are triggered when the component mounts. Here I created an animate prop for each circle, so that the animation runs when this prop changes.

The circle component could be something like this:

function Circle({ animate }) {
  const radius = useRef(new Animated.Value(50)).current;
  const borderRadius = Animated.divide(radius, 2);

  useEffect(() => {
    if (animate) {
      // if this circle is selected, we animate it
      Animated.timing(radius, {
        toValue: 100,
        duration: 500,
        useNativeDriver: false,
      }).start();
    }
    // if it isn't being selected we animate it back
    else {
      Animated.timing(radius, {
        toValue: 50,
        duration: 500,
        useNativeDriver: false,
      }).start();
    }
  }, [animate, radius]);

  return (
    <Animated.View
      style={{
        backgroundColor: 'blue',
        height: radius,
        width: radius,
        borderRadius: borderRadius,
      }}
    />
  );
}

We need only one useEffect which runs when animate changes. If it changes to true we should trigger the growing animation, if it changes to false we should trigger the shrinking animation.

Then, the parent component (Circles) could just control which circle is selected. I wrapped each circle in a Touchable just so it's easier to change the selected circle, and to trigger the animations:

const Circles = (props) => {
  const myCircles = [0, 1, 2, 3, 4, 5];
  const [biggerCircleIndex, setBiggerCircleIndex] = useState(2);

  return (
    <View>
      {myCircles.map((index) => {
        return (
          <TouchableOpacity onPress={() => setBiggerCircleIndex(index)}>
            <Circle animate={index === biggerCircleIndex} key={`${index}`} />
          </TouchableOpacity>
        );
      })}
    </View>
  );
};

There are probably other approaches to achieving this, maybe you could, as you suggested, make this work by creating an array of Animated.Value's.

I hope this helps!

Uploaded this code to a snack.

  • Related