3

I try to make a pulse animation view that loop infinite. The following view achieve that animation. But I would like to update the animation duration while it's animated. Actually It doesn't update the looping animations. Adding duration to the useEffect dependencies doesn't works properly because it's refreshed the currently animated circles.

Like this one (Source) :

Exemple

Here is my current code:

const LoopPulseView = (props: {duration: number;}) => {
  const nbPulse = 6;
  const [pulses, setPulses] = useState([] as any);
  const [duration, setDuration] = useState(props.duration);
  const [delay] = useState(duration / nbPulse);

  useEffect(() => {
    setDuration(props.duration);
  }, [props.duration]);


  const scaleOut = {
    0: {
      opacity: 1,
      scale: 1,
    },
    1: {
      opacity: 0,
      scale: 1.5,
    },
  };

  useEffect(() => {
    const newPulses = [];
    for (let p = 0; p < nbPulse; p++) {
      const view = (
        <Animatable.View
          key={p}
          duration={duration}
          delay={delay * p}
          style={[styles.pulse, pulseStyle]}
          animation={scaleOut}
          iterationCount={'infinite'}
          useNativeDriver={true}
        />
      );

      newPulses.push(view);
    }

    setPulses(newPulses);
  }, []);

  return (
    <View style={containerStyle}>
      <View style={pulseWrapperStyle}>
        {pulses.map((pulse: any) => {
          return pulse;
        })}
      </View>
    </View>
  );
};


export default LoopPulseView;
Ludovic
  • 1,992
  • 1
  • 21
  • 44
  • If you add 'duration' as a dependency of your useEffect, it will restart when the duration is modified – Kai Apr 23 '21 at 18:50
  • You are accessing duration from the prop and you are using it with the state. This is a no go. You don't need this line: const [duration, setDuration] = useState(props.duration); – Arbnor Apr 23 '21 at 20:05
  • @Kai yes but it reset the animation even if it's running, it's not smooth – Ludovic Apr 23 '21 at 20:27
  • @Arbnor Yes you right – Ludovic Apr 23 '21 at 20:28
  • 2
    @Ludovic You need to use the `transitionTo` imperative method to be able to change the duration. However, I think you'll find some unwanted side-effects from switching the animation duration. Check here for more [docs](https://github.com/oblador/react-native-animatable#transitiontotovalues-duration-easing) You should try implementing this animation from scratch using `react-native-reanimated`. You'll find that you have a lot more flexibility that way/ – Hamza Apr 27 '21 at 18:51
  • if you have the option to use https://reactnative.dev/docs/animated instead of Animatable.View. i can write worked example for you. – Ahmed Gaber May 01 '21 at 14:33

1 Answers1

3

You cannot change the props of your Animatable.View because it will trigger a render of your component.

You cannot replace all your Animatable.View because it will render your component too.

The solution is to create an array of animation where you will increase gradually the number of animations and replace the old ones.

I can give you an example.

import React, { useState, useEffect } from 'react';
import {
  View,
  StyleSheet,
  Button
} from 'react-native';

import * as Animatable from 'react-native-animatable';

const scaleOut = {
  0: {
    opacity: 1,
    scale: 1,
  },
  1: {
    opacity: 0,
    scale: 1.5,
  },
};

function Pulse({ duration }) {

  return (
    <Animatable.View
      duration={duration}
      animation={scaleOut}
      useNativeDriver={true}
    >
      <View
        style={[styles.pulse]}
      >
      </View>
    </Animatable.View>
  )

}

function App() {

  const [pulses, setPulses] = useState([]);
  const [nbPulse, setNbPulse] = useState(3);
  const [duration, setDuration] = useState(3000);

  useEffect(() => {
    setDelay(duration / nbPulse);
  }, [duration, nbPulse])

  const [delay, setDelay] = useState(duration / nbPulse);

  useEffect(() => {
    const interval = setInterval(() => {

      setPulses((prevState) => {

        // create a new pulse
        let pulse = { 
          id: String(Date.now()),
          duration: duration
        }

        let newState = [...prevState];
        // remove the first object when the limite of the length's array is reached
        if (newState.length >= nbPulse) { 
          newState.splice(0, newState.length - nbPulse);

        }
        // increase the size of the array
        newState.push(pulse); 

        return (newState);
      });

    }, delay);

    return () => {
      clearInterval(interval)
    };
  }, [delay]) // create a new intervale when delay is change

  return (

    <View
      style={{
        width: "100%",
        flex: 1
      }}>
      <Button
        onPress={() => { setNbPulse(nbPulse + 1) }}
        title="more pulse"
        color="#841584"
      />

      <Button
        onPress={() => {
          if (nbPulse > 1) {
            setNbPulse(nbPulse - 1)
          }
        }}
        title="less pulse"
        color="#841584"
      />

      <Button
        onPress={() => {
          setDuration(duration + 1000)
        }}
        title="pulse live increase"
        color="#841584"
      />

      <Button
        onPress={() => {
          if (duration > 1000) {
            setDuration(duration - 1000)
          }
        }}
        title="pulse live decrease"
        color="#841584"
      />

      {pulses.map((item) => (
        <Pulse
          key={item.id} // key to avoid rerender
          id={item.id}
          duration={item.duration} />
      ))}

    </View>

  );
}
const styles = StyleSheet.create({
  pulse: {
    backgroundColor: "red",
    position: 'absolute',
    top: 100,
    left: 100,
    height: 200,
    width: 200,
    borderRadius: 200
  }
});

export default App;
docmurloc
  • 1,201
  • 4
  • 11