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)