DEV Community

Cover image for React Native custom progress indicator with Reanimated library
Dima Portenko
Dima Portenko

Posted on • Updated on

React Native custom progress indicator with Reanimated library

Hello folks! In this tutorial I would like to show how create high performant custom progress indicator with react-native-reanimated library.

Video version of this tutorial available on YouTube.

Final gif of progress indicator

Let's start with following template on github or expo snack.

export const ProgressIndicator: FC<{
  count?: number;
  itemWidth?: number;
  itemHeight?: number;
  duration?: number;
  itemsOffset?: number;
  topScale?: number;
}> = ({
  count = 8,
  itemWidth = 16,
  itemHeight = 4,
  duration = 5000,
  itemsOffset = 4,
  topScale = 4,
}) => {
  return (
    <View
      style={{
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "center",
        height: itemHeight * topScale,
        width: (itemWidth + itemsOffset) * count,
      }}
    >
      {[...Array(count)].map((x, index) => (
        <ProgressItem
          key={`progressItem${index}`}
          index={index}
          width={itemWidth}
          height={itemHeight}
          count={count}
          topScale={topScale}
        />
      ))}
    </View>
  );
};

export const ProgressItem: FC<{
  index: number;
  count: number;
  width: number;
  height: number;
  topScale: number;
}> = ({ index, width, height, count, topScale }) => {
  return (
    <View
      style={[
        {
          width,
          height,
          backgroundColor: "black",
        },
      ]}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

So basically we have ProgressIndicator with styling props which renders count of ProgressItem components. ProgressItem is simply black rectangle. To achieve final result we will scale Y axis of rectangles in sequence.

First we need to add animated value and change it with timing animated function from react-native-reanimated library.

export const ProgressIndicator = ({duration, ...props}) => {
  const progress = useSharedValue(0);

  useEffect(() => {
    progress.value = withTiming(1, {
      duration,
    });
  }, []);
  // ...
};
Enter fullscreen mode Exit fullscreen mode

We created shared value progress with value 0. And start change it withTiming from 0 to 1 in duration milliseconds.

Now let's pass progress value to the ProgressItem and animate scale of Y axis.

// ...
<ProgressItem
  // ...
  progress={progress}
/>
// ...
Enter fullscreen mode Exit fullscreen mode

And use animated style in the Animated View.

  export const ProgressItem = ({ index, width, height, count, topScale, progress }) => {
  const animatedStyle = useAnimatedStyle(() => {

    const scaleY = interpolate(
      progress.value,
      [0, 1], // input progress value from 0 to 1
      [1, topScale], // output scale from 1 to 4 (topScale = 4)
      Extrapolation.CLAMP
    );
    return {
      transform: [{ scaleY }],
    };
  });
  return (
    <Animated.View
      style={[
        {
          width,
          height,
          backgroundColor: "black",
        },
        animatedStyle
      ]}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Basically what we did interpolate scaleY value from 1 to 4 inside useAnimatedStyle over duration milliseconds.

Scale from 1 to 4

Now instead of scale all items simultaneously let's do it one by one. For this we need to split our animation progress for the item count.

const scaleY = interpolate(
  progress.value,
  [index / count, (index + 1) / count],
  [1, topScale],
  Extrapolation.CLAMP
);
Enter fullscreen mode Exit fullscreen mode

Scale up one by one

Next step I would like to do is scale down before animate next item. In this case we need 3 output points [1, 4, 1] for each item.

const scaleY = interpolate(
  progress.value,
  [index / count, (index + 1) / count, (index + 2) / count],
  [1, topScale, 1],
  Extrapolation.CLAMP
);
Enter fullscreen mode Exit fullscreen mode

And here we are

Up and down animation

Now let's do our wave more smooth. We'll start next item animation earlier. To do it we split each progress piece by 3.

const parts = 3;
const wholeCount = count * 3; 
const scaleY = interpolate(
  progress.value,
  [index / wholeCount, (index + parts) / wholeCount, (index + 2 * parts) / wholeCount],
  [1, topScale, 1], 
  Extrapolation.CLAMP
);
Enter fullscreen mode Exit fullscreen mode

Almost what we want but animations finished a bit earlier than our duration. To use whole duration our 3 point value for last index should be equal 1.

(index + 2 * parts) / wholeCount = 1
// where 
index = count - 1
// so
wholeCount = count - 1 + 2 * parts;
Enter fullscreen mode Exit fullscreen mode

then

const parts = 3;
const wholeCount = count - 1 + 2 * parts; 
const scaleY = interpolate(
  progress.value,
  [index / wholeCount, (index + parts) / wholeCount, (index + 2 * parts) / wholeCount],
  [1, topScale, 1], 
  Extrapolation.CLAMP
);
Enter fullscreen mode Exit fullscreen mode

Image description

Great! We are almost there. Our animation looks good, but now it plays only once after component mount. To fix this let's use withRepeat animation function. And we'll wrap our withTiming function like this

useEffect(() => {
  progress.value = withRepeat(
    withTiming(1, { duration }),
    -1,
    true
  );
}, []);
Enter fullscreen mode Exit fullscreen mode

First argument is our withTiming function. Second is number of repetitions if negative it will be infinity loop. Third is reverse param, which means it will play our animations back and forth. Basically it will change our progress value from 0 to 1, then from 1 to 0 and repeat. In case reverse is false it will change value from 0 to 1, jump back and 0 to 1 again.

Final gif of progress indicator

That's it! Please let me know what you think in the comments and of course feel free to ask any questions. Final code is available on snack and on github.

Discussion (0)