The story about one progress bar animation with React Native
Tags: reactnative, react, javascript, tutorial
Published: 2020-06-24 14:21:50

   I had a chance to work closely with animations in web applications and mobile(React Native). Sometimes I was confused by complex react native animations and didn't understand why people created *react-native-reanimated* library.
   In this tutorial I wanted to explain the difference in web/mobile animations, benefits from *react-native-reanimated*, cross-platform React components. 

   Let's see how to build an animated progress bar in a browser.

   Firstly, we create a React component:

```jsx
 const ProgressBar = ({ total, current, fill }) => {
   const percent = current / total;
   return (
     <div class="container">
       <div class="progress"></div>
     </div>
   )
 }
```

   CSS styles:

```css
.container {
  background-color: #eee;
  height: 4px;
  border-radius: 2px;
  margin: 20px;
  position: relative;
  overflow: hidden;
}

.progress {
  position: absolute;
  left: -100%;
  width: 100%;
  top: 0;
  bottom: 0;
  border-radius: 2px;
}
```

   Next step is applying styles from props to our layout:

```jsx
...
  <div class="progress" style={{ transform: `translateX(${percent * 100}%)`, backgroundColor: fill }}></div>
...
```

   Let's see the result:

![Progress bar](https://raw.githubusercontent.com/serzmerz/react-native-progress-bar/master/images/progress1.png)

## What about animations?

   In web applications, it's really easy to animate properties like transform or background color, the browser will do all stuff for animation without us.

   Just add *transition* property to our styles:

```css
.progress {
  ...
  transition: all 0.2s;
}
```

   That's a result:

![Progress bar animation](https://raw.githubusercontent.com/serzmerz/react-native-progress-bar/master/images/gif1.gif)

### Seems like component very easy, why I show you this example?

   Let's try to implement this component with React Native:

```jsx
const AnimatedProgress = ({ fill, current, total }) => {
     const percent = current / total;
     
     return (
       <View style={styles.container}>
         <View style={[styles.progress, { backgroundColor: fill, transform: [{ translateX: `${percent * 100}%` }] }]} />
       </View>
     );
   };
```

   Ooops, our component doesn't work as expected, because *translateX* must be a *number* (from documentation).

   So, how we can get the width of the element?

   Let's add *useOnLayout* hook:

```js
export default function useOnLayout() {
  const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
  const onLayout = useCallback((event) => setLayout(event.nativeEvent.layout), []);

  return [layout, onLayout];
}
```

   Pass *onLayout* handler to our wrapper *View*:

```jsx
const AnimatedProgress = ({ fill, current, total }) => {
     const percent = current / total;
     
    // we need only width property
     const [{ width }, onLayout] = useOnLayout();
     
     return (
       <View style={styles.container} onLayout={onLayout}>
         <View style={[styles.progress, { backgroundColor: fill, transform: [{ translateX: width * percent }] }]} />
       </View>
     );
   };
```

   Next step is animate our *translateX* property:

```
import { Animated } from "react-native";

// wrap our Animated.Value to useState for memoize it, alternatively you can use useRef
const [translateX] = useState(new Animated.Value(0));
   
useEffect(() => {
 Animated.timing(translateX, {
   toValue: width * percent,
   duration: 200,
   easing: Easing.inOut(Easing.ease),
   // using native driver for animation in UI thread 
   useNativeDriver: true
   }).start();
// call our animation when width or percent change
}, [width, percent]);

....

// Change View => Animated.View and translateX to our Animated.Value
<Animated.View style={[styles.progress, { backgroundColor: fill, transform: [{ translateX }] }]} />
```

### Last animation for today - backgroundColor animation.

   Is it so easy like in web applications?

   Before we start writing color animation let's switch to *react-native-reanimated* library, which more flexible and has much more defined functions:

*react-native-reanimated* has backward capability API, so we can easily move to it without rewriting our codebase.
* Note: we removed *useNativeDriver: true* property from config because *react-native-reanimated* already run all animations in the native UI thread.

```js
import Animated, { Easing } from "react-native-reanimated";

useEffect(() => {
    Animated.timing(translateX, {
      toValue: width * percent,
      duration: 200,
      easing: Easing.inOut(Easing.ease)
    }).start();
  }, [width, percent]);
```

   The main problem with animating colors in react native is a lack of transitions similar to the browser. Honestly, browsers do lots of stuff under the hood, like all those animations with CSS, interpolation colors, etc.

   So, we should create a transition for color by ourselves. Previously we switched to a *react-native-reanimated* library, it has many useful functions which we will use.
   Imagine the flow of color interpolation:

    1) Convert color: Firstly let's convert color to one format - for example, RGBA(you can choose a different one - HSV, HSL)
    2) we need steam of animation which we can iterate - it can be usual for us *Animated.Value*, which we can animate from 0 to some value.
    3) In this time we will make interpolation of animation this value each of color part (r, g, b, a);
    4) combine all these values into one color.
 
   One important requirement - animations should work in the native UI thread. That's why we can't use simple interpolation from *react-native* to string colors, like this:

 ```js
 const interpolateColor = animatedValue.interpolate({
   inputRange: [0, 150],
   outputRange: ['rgb(0,0,0)', 'rgb(51, 250, 170)']
 })
```

   Native UI thread animations had a strong limitation, you can only animate non-layout properties, things like transform and opacity will work but Flexbox and position properties won't.
   That's because we should define our animation before start it.

   More about animations you can read here: https://reactnative.dev/blog/2017/02/14/using-native-driver-for-animated

   We want to run animation when our prop fill has changed, for this reason, we should store previous fill value for running our interpolation.

   Create the custom hook for color interpolation, and store previous value:

```js
export default function useAnimatedColor(color) {  
  // store our previous color in ref
  const prevColor = useRef(color);
  
  // TODO write color interpolation

  // updating previous value after creating interpolation
  prevColor.current = color;
 
  // TODO return color interpolation
  return color;
}
```

   The next step is adding color value which we will interpolate and run animation on color change. Of course, we can use *useEffect* from *react-native* for it, but *react-native-reanimated* has their own *useCode* hook.

```js
// define input range for interpolation
const inputRange = [0, 50];

export default function useAnimatedColor(color) {
  // store our value to ref for memoization
  const colorValue = useRef(new Animated.Value(0));
  ...
  useCode(() => {
      const [from, to] = inputRange;
      // TODO iterate colorValue in range
    }, [color]);
}
```

*react-native-reanimated* has his mechanism for control each frame tick - *Clock*. And common function *runTiming* - for timing animation(it contain lots of boilerplate, you can find source code in the documentation or full code of this tutorial).
https://github.com/serzmerz/react-native-progress-bar

```js
import Animated, { Clock } from "react-native-reanimated";

const { set, useCode } = Animated;

export default function useAnimatedColor(color) {
  const colorValue = useRef(new Animated.Value(0));
  ...
  // create clock instance and memoize it
  const clock = useRef(new Clock());
  
    useCode(() => {
      const [from, to] = inputRange;
      return [set(colorValue.current, runTiming(clock.current, from, to))];
    }, [color]);
}
```

   Last thing which we do in this hook - colors interpolation, full code of this hook bellow:

```js
const inputRange = [0, 50];

export default function useAnimatedColor(color) {
  const colorValue = useRef(new Animated.Value(0));
  const prevColor = useRef(color);
  
  // call our interpolateColors and wrap it to useMemo
  const backgroundColor = useMemo(
    () =>
      interpolateColors(colorValue.current, {
        inputRange,
        outputColorRange: [prevColor.current, color]
      }),
    [color]
  );

  prevColor.current = color;

  const clock = useRef(new Clock());

  useCode(() => {
    const [from, to] = inputRange;
    return [set(colorValue.current, runTiming(clock.current, from, to))];
  }, [color]);

  return backgroundColor;
}
```

   What about *interpolateColors* function. For now, *react-native-reanimated* has implemented it in the codebase, but not published. if you read this tutorial, and the version of *react-native-reanimated* is above 1.9.0, this function should be inside.

   By the way, we deep dive to this function for understanding how it works:

```js
import { processColor } from "react-native";
import Animated, { round, color, interpolate, Extrapolate } from "react-native-reanimated";

// functions for getting each part of our color
function red(c) {
  return (c >> 16) & 255;
}
function green(c) {
  return (c >> 8) & 255;
}
function blue(c) {
  return c & 255;
}
function opacity(c) {
  return ((c >> 24) & 255) / 255;
}

/**
 * Use this if you want to interpolate an `Animated.Value` into color values.
 *
 * #### Why is this needed?
 *
 * Unfortunately, if you'll pass color values directly into the `outputRange` option
 * of `interpolate()` function, that won't really work (at least at the moment).
 * See https://github.com/software-mansion/react-native-reanimated/issues/181 .
 *
 * So, for now you can just use this helper instead.
 */
export default function interpolateColors(animationValue, options) {
  const { inputRange, outputColorRange } = options;
  // convert our colors to rgba format 
  const colors = outputColorRange.map(processColor);

  // interpolate each part of our color
  const r = round(
    interpolate(animationValue, {
      inputRange,
      // map only necessary part 
      outputRange: colors.map(red),
      extrapolate: Extrapolate.CLAMP
    })
  );
  const g = round(
    interpolate(animationValue, {
      inputRange,
      outputRange: colors.map(green),
      extrapolate: Extrapolate.CLAMP
    })
  );
  const b = round(
    interpolate(animationValue, {
      inputRange,
      outputRange: colors.map(blue),
      extrapolate: Extrapolate.CLAMP
    })
  );
  const a = interpolate(animationValue, {
    inputRange,
    outputRange: colors.map(opacity),
    extrapolate: Extrapolate.CLAMP
  });

  // combine all parts to one color interpolation
  return color(r, g, b, a);
}

```

   That's it, you can call our hook inside *AnimatedProgress* component:

```js
const AnimatedProgress = ({ fill, current, total }) => {
  const backgroundColor = useAnimatedColor(fill);

  ...
  // pass animated props to view
  <Animated.View style={[styles.progress, { backgroundColor, transform: [{ translateX }] }]} />
  ...
}
```

## Have you noticed that the layout for Web and mobile are the same?

   The last thing for today is making progress bar component cross-platform.
   For achieving this goal we need to do two steps:
    1) Split our hooks to two hooks:
       - useAnimatedColor.js/useAnimatedColor.native.js
       - useAnimatedProgress.js/useAnimatedProgress.native.js

.native.js extensions will load by the metro bundler on mobile platforms.
.js extensions will load on web.

   For the web, we just make these hooks simple. All animations will be done by transition property.

   useAnimatedColor.js:

```js
export default function useAnimatedColor(color) {
 return color;
}
```

   useAnimatedProgress.js

```js
export default function useAnimatedProgress(width, percent) {
  return width * percent;
}
```

   2) Add transition for web application in styles:

```js
export default StyleSheet.create({
  ...
  progress: {
    ...
    // select only web for avoiding error on mobile devices
    ...Platform.select({ web: { transition: "0.3s all ease-in-out" } })
  }
});
```

   Wow! We've built a cross-platform component with native animations for each platform.

   You can find all source code in github: https://github.com/serzmerz/react-native-progress-bar

   Example of usage: https://github.com/serzmerz/TestReactNativeProgressBar

   And install the finished library for your own purpose.

```js
yarn add react-native-reanimated-progress-bar
```