'React Native: Fade through a series of images using Animated & opacity

I'm currently trying to fade through a series of images. Basically, I always want to display one image at a time, then animate its opacity from 1 to 0 and that of the next image in the series from 0 to 1, and so on. Basically something like this, which I've already implemented for the web in ReactJS and CSS animations:

Fade animation in ReactJS

However, I seem to keep getting stuck on using React Native's Animated library and refs. I've tried storing the opacity of all the images in an array which itself is contained in an useRef hook. Then, using Animated, I'm trying to perform two parallel animations which change the opacity of the current image index and that of the next index. This is what I've come up with:

export default function StartImageSwitcher() {
  const images = Object.values(Images).map((img) => img.imageNoShadow);

  const [currentImage, setCurrentImage] = useState(0);
  const opacity = useRef<Animated.Value[]>([
    new Animated.Value(1),
    ...Array(images.length - 1).fill(new Animated.Value(0)),
  ]).current;

  useEffect(() => {
    let nextImage = currentImage + 1;
    if (nextImage >= images.length) nextImage = 0;

    Animated.parallel([
      Animated.timing(
        opacity[currentImage],
        {
          toValue: 0,
          duration: 2000,
          useNativeDriver: true,
        },
      ),
      Animated.timing(
        opacity[nextImage],
        {
          toValue: 1,
          duration: 2000,
          useNativeDriver: true,
        },
      ),
    ]).start(() => {
      setCurrentImage(nextImage);
    });
  }, [currentImage]);

  images.map((image, index) => console.log(index, opacity[index]));

  return (
    <View style={styles.imageWrapper}>
      {
      images.map((image, index) => (
        <Animated.Image style={{ ...styles.image, opacity: opacity[index] }} source={image} key={index} />
      ))
    }
    </View>
  );
}

However, this doesn't seem to work at all. When mounted, it only shows the first image, then fades that one out and all the other images in and gets stuck there:

Image clutter using the above code in React Native

Anyone got an idea where I messed up? I feel like I'm not using the useRef() hook in combination with the Animated library like I'm supposed to.



Solution 1:[1]

Your solution is pretty clever, and it generally looks like it should work to me. Performance might take a hit though as the number of images increases. I thought of an alternate method: partition the images array and then use setInterval to alternate between two Animated.Images, which get their source from each array.

// from https://stackoverflow.com/questions/41932345/get-current-value-of-animated-value-react-native
const getAnimatedValue = (value: Animated.Value) => Number.parseInt(JSON.stringify(value));

export default function StartImageSwitcher() {
  // partition images into two arrays
  const images1 = [];
  const images2 = [];
  Object.values(Images).forEach((img, i) => {
    (i % 1 === 0 ? images1 : images2).push(img.imageNoShadow)
  });

  // use refs for the indexes so values don't become stale in the setInterval closure
  const images1Index = useRef(0);
  const images2Index = useRef(0);
  const image1Opacity = useRef(new Animated.Value(1)).current;
  const image2Opacity = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const swapImageInterval = setInterval(() => {
      const newImage1Opacity = getAnimatedValue(image1Opacity) === 1 ? 0 : 1;
      const newImage2Opacity = getAnimatedValue(image2Opacity) === 1 ? 0 : 1;

      Animated.parallel([
        Animated.timing(image1Opacity, {
          toValue: newImage1Opacity,
          duration: 2000,
          useNativeDriver: true,
        }),
        Animated.timing(image2Opacity, {
          toValue: newImage2Opacity,
          duration: 2000,
          useNativeDriver: true,
        }),
      ]).start(() => {
        if (newImage1Opacity === 1) {
          // image 2 is now faded out, so we can swap out its source
          const nextIndex = images2Index.current === images2.length - 1 ? 0 : images2Index.current + 1;
          images2Index.current = nextIndex;
        } else {
          // image 1 is faded out, so we can swap out its source 
          const nextIndex = images1Index.current === images1.length - 1 ? 0 : images1Index.current + 1;
          images1Index.current = nextIndex;
        }
      })
    }, 5000)

    return () => clearInterval(swapImageInterval);
  }, [images1Index, images2Index]);

  return (
    <View style={styles.imageWrapper}>
      <Animated.Image style={{ ...styles.image, opacity: image1Opacity }} source={images1[images1Index.current]} />
      <Animated.Image style={{ ...styles.image, opacity: image2Opacity }} source={images2[images2Index.current]} />
    </View>
  );
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Abe