Search code examples
react-nativereact-native-reanimated

How to create animated gradients in React Native?


I'm trying to create an animated gradient for my expo app. I know this can be done for websites using pure CSS:

.container {
background: linear-gradient(45deg, "red", "green", "blue", "yellow")
animation: color 12s ease-in-out infinite
}

But I'm not able to figure out how to do it in React Native. I was originally using expo-linear-gradient for rendering the Linear gradient but apparently that package can't be used for creating animations so I switched to React Native Skia (with Reanimated). I tried for many hours but I still wasn't able to get that effect. And I'm not able to find anything online.

Can someone give a hint on how this could be achieved.


Solution

  • Animations are the one thing react-native differs greatly from react in. I recommend using react-native-reanimated for animations. Going with react native skia for animating gradients seemed to be the right move. I tried to use reanimated's createAnimatedComponent for expo-linear-gradient and couldn't get to work, so I went with react native skia. Most Skia components accepts SharedValues so the animating them is just doing reanimated stuff to your values:

    import {
      colorManipulators,
    } from "@phantom-factotum/colorutils";
    import {
      Canvas,
      LinearGradient,
      Rect,
      vec
    } from '@shopify/react-native-skia';
    import { ReactNode, useEffect, useMemo, useState } from 'react';
    import { LayoutRectangle, StyleSheet, View, ViewStyle } from 'react-native';
    import { interpolateColor, useDerivedValue, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
    
    type Props = {
      contentContainerStyle?: ViewStyle;
      children: ReactNode;
      colors: string[];
      start:[number,number]
      end:[number,number]
      // style?:ViewStyle;
    };
    
    export default function AnimatedGradient({
      contentContainerStyle,
      children,
      colors,
      start,
      end
    }: Props) {
      // store children layout properties
      const [layout, setLayout] = useState<LayoutRectangle>({
        width: 0,
        height: 0,
        x: 0,
        y: 0,
      });
      const animValue = useSharedValue(0)
      const darkColors = useMemo(()=>colors.map(color=>colorManipulators.blend(color,'black',0.5)),[colors])
      const animatedColors = useDerivedValue(()=>{
        return colors.map((color,i)=>interpolateColor(animValue.value,[0,1],[color,darkColors[i]]))
      })
     
      useEffect(()=>{
        animValue.value = withRepeat(
          withTiming(1,{duration:500}),
          -1,
          true
        )
      },[])
      return (
        <>
          <Canvas
            style={{
              // Canvas can only have skia elements within it
              // so position it absolutely and place non-skia elements
              // on top of it
              position: 'absolute',
              width:layout.width,
              height:layout.height
            }}>
              <Rect x={0} y={0} width={layout.width} height={layout.height} strokeWidth={1}>
                <LinearGradient
                  colors={animatedColors}
                  origin={vec(layout.width/2, layout.height/2)}
                  start={vec(layout.width*start[0],layout.height*start[1])}
                  end={vec(layout.width*end[0],layout.height*end[1])}
                  />
              </Rect>
          </Canvas>
          <View
            style={styles.contentContainer}
            onLayout={(e) => setLayout(e.nativeEvent.layout)}>
            {children}
          </View>
        </>
      );
    }
    
    const styles = StyleSheet.create({
      contentContainer: {
        backgroundColor: 'transparent',
      },
    });