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
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",
},
]}
/>
);
};
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,
});
}, []);
// ...
};
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}
/>
// ...
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
]}
/>
);
};
Basically what we did interpolate scaleY value from 1 to 4 inside useAnimatedStyle over duration milliseconds.
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
);
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
);
And here we are
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
);
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;
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
);
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
);
}, []);
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.
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.





Top comments (0)