React-native-reanimated: v2 Animate Colors

Created on 31 May 2020  路  20Comments  路  Source: software-mansion/react-native-reanimated

Description

Animating colors with withTiming or withSpring doesn't seem to work with colors values (hex or RGB).

Screenshots

Kapture 2020-05-30 at 17 28 53

Steps To Reproduce

1.

Expected behavior

Animate colors

Actual behavior

Color just changes

Snack or minimal code example

import Animated, {
  useSharedValue,
  withSpring,
  useAnimatedStyle,
  Easing,
  withTiming,
} from 'react-native-reanimated';
import {View, Button} from 'react-native';
import React from 'react';

function getColor() {
  return (
    '#' +
    ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6)
  );
}
function hexToRGB(h) {
  let r = 0,
    g = 0,
    b = 0;

  // 3 digits
  if (h.length == 4) {
    r = '0x' + h[1] + h[1];
    g = '0x' + h[2] + h[2];
    b = '0x' + h[3] + h[3];

    // 6 digits
  } else if (h.length == 7) {
    r = '0x' + h[1] + h[2];
    g = '0x' + h[3] + h[4];
    b = '0x' + h[5] + h[6];
  }

  return 'rgb(' + +r + ',' + +g + ',' + +b + ')';
}

export default function AnimatedStyleUpdateExample(props) {
  const randomWidth = useSharedValue(10);
  const color = useSharedValue(hexToRGB(getColor()));
  const style = useAnimatedStyle(() => {
    return {
      width: withSpring(randomWidth.value),
      backgroundColor: withTiming(color.value, {
        duration: 500,
        easing: Easing.bezier(0.5, 0.01, 0, 1),
      }),
    };
  });

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Animated.View style={[{width: 100, height: 80, margin: 30}, style]} />
      <Button
        title="toggle"
        onPress={() => {
          color.value = hexToRGB(getColor());
          randomWidth.value = Math.random() * 350;
        }}
      />
    </View>
  );
}

Package versions

  • React:
  • React Native: v2 Playground repo
  • React Native Reanimated: v2
馃彔 Reanimated2 馃悶 Bug

All 20 comments

@browniefed, why did you close this?

@jakub-gonet ah I was doing it wrong, should be interpolating. Although I still don't think top level coloring is handled after reviewing some stuff

Cool, thanks for the response, if you find another issue regarding colors feel free to reopen.

Has anyone managed to get colors to work? If I put a color in withSpring, I see this error:

Screen Shot 2020-11-14 at 7 42 03 PM

@nandorojo, please show us code that is crashing, we can't help you based only on the stack trace.

Yup sorry about that, I'll add a code sample here.

This code produces that error:

import React, { useReducer } from 'react'
import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated'
import { Button } from 'react-native'

export default function ColorError() {
  const [backgroundColor, toggle] = useReducer(
    (bg) => (bg === 'red' ? 'green' : 'red'),
    'red'
  )
  const style = useAnimatedStyle(() => ({
    backgroundColor: withSpring(backgroundColor),
  }))
  return (
    <Animated.View
      style={[
        {
          flex: 1,
          alignItems: 'center',
          justifyContent: 'center',
        },
        style,
      ]}
    >
      <Button title="Toggle Color" onPress={toggle} color="white" />
    </Animated.View>
  )
}

I'm using the expo reanimated starter. Here's my package.json:

{
  "version": "39.0.0",
  "dependencies": {
    "expo": "~39.0.0",
    "expo-status-bar": "~1.0.2",
    "react": "~16.13.0",
    "react-dom": "~16.13.0",
    "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.2.tar.gz",
    "react-native-gesture-handler": "^1.8.0",
    "react-native-reanimated": "2.0.0-alpha.6.1",
    "react-native-web": "0.13.13",
    "react-navigation-stack": "^2.8.4"
  },
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "babel-preset-expo": "~8.1.0",
    "eslint-config-nando": "^1.0.10"
  },
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo web",
    "eject": "expo eject"
  },
  "private": true
}

I also get this TS error:

Screen Shot 2020-11-18 at 10 17 23 AM

withSpring takes a number as an argument, you can't use a string there. What do you want to achieve here? If you want to interpolate between two colors you can use interpolateColors (which is undocumented, but it should work).

Got it, that makes sense. I'm basically trying to animate from one color to the next, where I keep the color as a React state value. Whenever the color changes, I animate to the next one.

I'm working on a component library similar to framer-motion, powered by reanimated 2.

Link: https://github.com/nandorojo/redrip
Background: https://github.com/nandorojo/dripsy/issues/46

I鈥檓 hoping to pass backgroundColor to a custom animate prop. Whenever it changes, it automatically animates, similar to transition-property: background-color in CSS.

If you're interested, you can see how I'm doing it in src/redripify/use-map-animate-to-style.ts

I assume interpolateColors only works for interpolating based on a given animate node. In my case, however, I'm not creating any animated nodes -- instead, I'm using useAnimatedStyle directly, as shown in my example above.

Maybe I could create an animated value under the hood for every possible color style value, but it doesn't seem ideal.

Do you have any suggestions @jakub-gonet? Thanks so much for your time!

I don't maintain Rea2 and have limited knowledge about it but @zrebcu411 or @piaskowyk maybe know the answer for this one?

I tried using processColor for the color values. However, this leads to an odd flickering.

import { View, Button, StyleSheet } from 'react-native'
import React from 'react'
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  processColor, // I tried this
} from 'react-native-reanimated'

const colors = ['rgb(5,200,3)', 'rgb(10,5,100)']

export default function ColorBug() {
  const color = useSharedValue(colors[0])

  const toggleColor = () => {
    if (color.value === colors[0]) {
      color.value = colors[1]
    } else {
      color.value = colors[0]
    }
  }

  const style = useAnimatedStyle(() => ({
    backgroundColor: withTiming(processColor(color.value)),
  }))

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, style]}></Animated.View>
      <Button title="toggle" onPress={toggleColor} />
    </View>
  )
}

const styles = StyleSheet.create({
  box: {
    justifyContent: 'center',
    backgroundColor: 'blue',
    height: 100,
    width: 100,
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
  },
})

I tried using hex strings, RGB strings, and color names in the colors array, but they all have the same result.

Here's a video of what happens:

ezgif com-gif-maker

@jakub-gonet would you mind re-opening this?

Sure, but I'm afraid I won't be able to help.

Got it, thanks for the heads up! I'll keep an eye out here.

Hi @nandorojo!

You should move withTiming(processColor(color.value)) from useAnimatedStyle to different method and use color.value instead, and it will work
Here is code snippet
```
const colors = ['rgba(5, 200, 3, 1)', 'rgba(10, 5, 100, 1)']
....
const toggleColor = () => {
if (color.value === colors[0]) {
color.value = withTiming(processColor(colors[1]))
} else if (color.value === colors[1]) {
color.value = withTiming(processColor(colors[0]))
}
}

const style = useAnimatedStyle(() => ({
backgroundColor: color.value,
}))
```

FYI tested this case on ios, android and web
On both mobile platforms bug is reproducible
On web I just see empty screen (screenshot attached)
When moved withTiming(processColor(color.value)) from useAnimatedStyle, animation works correctly on all of the platforms

Working Code:

import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
  processColor,
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';
import React from 'react';

const colors = ['rgba(5,200,3, 1)', 'rgba(10,5,100,1)']

export default function ColorBug() {
  const color = useSharedValue(colors[0])

  const toggleColor = () => {
    if (color.value === colors[0]) {
      color.value = withTiming(processColor(colors[1]))
    } else {
      color.value = withTiming(processColor(colors[0]))
    }
  }

  const style = useAnimatedStyle(() => ({
    backgroundColor: color.value,
  }))

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, style]}></Animated.View>
      <Button title="toggle" onPress={toggleColor} />
    </View>
  )
}

const styles = StyleSheet.create({
  box: {
    justifyContent: 'center',
    backgroundColor: 'blue',
    height: 100,
    width: 100,
  },
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
  },
})

image

@dmmaslenn Thanks for testing that, I'll try this out.

I'm actually working on a library that needs to drive the animation inside of the UAS hook, so it would be great to have this bug fixed. I really appreciate you finding the source of it.

Which version are you on, by the way?

I wonder if this could be related to https://github.com/software-mansion/react-native-reanimated/issues/1511?

Hey @nandorojo I鈥檓 sorry for my late reply.
Well basically it isn't a bug 馃榿 When you use processColor(colorStr) it is converted to a number. In the next step you try to animate this value but this is a number not a color, and interpolation of number is different then interpolation of color. In summary why it was acting strange - because if you want to interpolate color in smooth way you should interpolate each channel (R, G, B, A) separately not as one 32bit number

You have two simple solutions:
Based on your example code. You should just remove processColor()


Code

import { View, Button, StyleSheet } from 'react-native'
import React from 'react'
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated'

const colors = ['rgb(5,200,3)', 'rgb(10,5,100)']

export default function ColorBug() {
  const color = useSharedValue(colors[0])

  const toggleColor = () => {
    if (color.value === colors[0]) color.value = colors[1]
    else color.value = colors[0]
  }

  const style = useAnimatedStyle(() => ({
    backgroundColor: withTiming(color.value),
  }))

  return (
    <View>
      <Animated.View style={[styles.box, style]}></Animated.View>
      <Button title="toggle" onPress={toggleColor} />
    </View>
  )
}

const styles = StyleSheet.create({
  box: {
    justifyContent: 'center',
    backgroundColor: 'blue',
    height: 100,
    width: 100,
  },
})

My recommended way is use interpolateColor:


Code

import { View, Button, StyleSheet } from 'react-native'
import React from 'react'
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  interpolateColor,
  Easing
} from 'react-native-reanimated'

const colors = ['rgb(5,200,3)', 'rgb(10,5,100)']

export default function ColorBug() {
  const color = useSharedValue(0)

  const toggleColor = () => {
    if (color.value === 0) {
      color.value = withTiming(1, {duration: 200, easing: Easing.linear})
    } else {
      color.value = withTiming(0, {duration: 200, easing: Easing.linear})
    }
  }

  const style = useAnimatedStyle(() => ({
    backgroundColor: interpolateColor(
      color.value,
      [0, 1],
      colors,
    ),
  }))

  return (
    <View>
      <Animated.View style={[styles.box, style]}></Animated.View>
      <Button title="toggle" onPress={toggleColor} />
    </View>
  )
}

const styles = StyleSheet.create({
  box: {
    justifyContent: 'center',
    backgroundColor: 'blue',
    height: 100,
    width: 100,
  },
})

I tested this on the latest version of Reanimated.

Have you any more questions or can I close this issue?

@piaskowyk Thanks! The only open question 鈥撀營'm passing a string to withTiming, but it expects a number. Should I @ts-ignore?

I think that we should change the passing type in the signature of function. I think we will change this in the newer version.

Was this page helpful?
0 / 5 - 0 ratings