March 13, 2023
Here we have a slider. When user slides the slider then the loader needs to show how much is loaded. We want to animate the loader component when the slider moves. In other words the loader animation is "dependent" on the slider animation.
This is an example of Dependent Animations on the UI thread. Dependent Animation is when one view is animated based on some another element.
first we need a gesture handler to detect the slider events. Then based on the slider events we need to animate the progress of loader component.
const [width, setWidth] = useState(200);
const x = useSharedValue(0);
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
x.value = event.absoluteX;
},
onActive: (event, ctx) => {
x.value = event.absoluteX;
// We need to calculate `progress` here based on the slider position.
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: x.value,
},
],
};
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[{ height: 20 }]}>
<Animated.View
pointerEvents="none"
style={[
{
backgroundColor: "blue",
height: 20,
width: 20,
borderRadius: 10,
},
animatedStyle,
]}
/>
<View
pointerEvents="none"
style={{
backgroundColor: "black",
height: 2,
width: "100%",
position: "absolute",
top: 10,
}}
/>
</Animated.View>
</PanGestureHandler>
);
Loader component:
<View style={{ height: 20, marginTop: 10 }}>
<Lottie
style={{
alignSelf: "center",
width: "100%",
}}
progress={"Calculated `progress` based on slider position."}
source={require("./progress")}
autoPlay={false}
loop={false}
/>
</View>
Before we move further, let's understand the difference between UI Thread and JS Thread. The UI Thread handles rendering and gestures of Android and iOS views, whereas the JS Thread takes care of all the logic of the React Native application.
We have two approaches for animating the loader.
In the first approach, when the slider moves, we can store the progress
in
react state and pass it to Lottie animation. With
this approach, the entire component rerenders on setting the progress
.
const [progress, setProgress] = useState(0);
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
x.value = event.absoluteX;
},
onActive: (event, ctx) => {
x.value = event.absoluteX;
runOnJS(setProgress)(x.value / width);
},
});
<Lottie
style={{
alignSelf: "center",
width: "100%",
}}
progress={progress}
source={require("./progress")}
autoPlay={false}
loop={false}
/>;
In the second approach, when the slider moves, we can calculate progress
and
pass it to the loader component via the
useAnimatedProps
.
In this way, the progress
gets calculated on the UI thread itself. Hence it
avoids rerenders.
const LottieAnimated = Animated.createAnimatedComponent(Lottie);
const [progress, setProgress] = useState(0);
const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width);
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
x.value = event.absoluteX;
},
onActive: (event, ctx) => {
x.value = event.absoluteX;
},
});
<LottieAnimated
style={{
alignSelf: "center",
width: "100%",
}}
animatedProps={lottieAnimatedProps}
source={require("./progress")}
autoPlay={false}
loop={false}
/>;
With the first approach, whenever the slider moves, the UI thread will pass the
gesture event to the JS thread to store the progress
value in react state, and
when the progress
value changes in the JS thread, it causes re-render. This
approach creates a lot of traffic over
Communication Bridge
because of message exchange between UI and JS threads.
So we should prefer the second approach to run any calculations on the UI thread instead of the JS thread. Here is the entire code for reference:
let approach1ReRenderCount1 = 0;
const Approach1: () => Node = () => {
const [width, setWidth] = useState(1);
const x = useSharedValue(0);
const [progress, setProgress] = useState(0);
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
x.value = event.absoluteX;
},
onActive: (event, ctx) => {
x.value = event.absoluteX;
runOnJS(setProgress)(x.value / width);
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: x.value,
},
],
};
});
return (
<SafeAreaView>
<View
style={{
height: 150,
paddingHorizontal: 20,
borderWidth: 1,
margin: 10,
justifyContent: "center",
}}
onLayout={({
nativeEvent: {
layout: { width },
},
}) => {
setWidth(width);
}}
>
<Text style={{ fontSize: 20, paddingBottom: 10 }}>Approach 1</Text>
<Text style={{ fontSize: 20 }}>
Rerender Count : {approach1ReRenderCount1++}
</Text>
<View style={{ height: 20, marginTop: 10 }}>
<LottieAnimated
style={{
alignSelf: "center",
width: "100%",
}}
progress={progress}
source={require("./progress")}
autoPlay={false}
loop={false}
/>
</View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[{ height: 20 }]}>
<Animated.View
style={[
{
backgroundColor: "blue",
height: 20,
width: 20,
borderRadius: 10,
},
animatedStyle,
]}
/>
<View
style={{
backgroundColor: "black",
height: 2,
width: "100%",
position: "absolute",
top: 10,
}}
/>
</Animated.View>
</PanGestureHandler>
</View>
</SafeAreaView>
);
};
const LottieAnimated = Animated.createAnimatedComponent(Lottie);
let approach2ReRenderCount = 0;
const Approach2: () => Node = () => {
const [width, setWidth] = useState(1);
const x = useSharedValue(0);
const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width);
const gestureHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
x.value = event.absoluteX;
},
onActive: (event, ctx) => {
x.value = event.absoluteX;
},
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: x.value,
},
],
};
});
return (
<SafeAreaView>
<View
style={{
height: 150,
paddingHorizontal: 20,
borderWidth: 1,
margin: 10,
justifyContent: "center",
}}
onLayout={({
nativeEvent: {
layout: { width },
},
}) => {
setWidth(width);
}}
>
<Text style={{ fontSize: 20, paddingBottom: 10 }}> Approach 2</Text>
<Text style={{ fontSize: 20 }}>
Rerender Count : {approach2ReRenderCount++}
</Text>
<View style={{ height: 20, marginTop: 10 }}>
<LottieAnimated
animatedProps={lottieAnimatedProps}
style={{
alignSelf: "center",
width: "100%",
}}
source={require("./progress")}
autoPlay={false}
loop={false}
/>
</View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[{ height: 20 }]}>
<Animated.View
pointerEvents="none"
style={[
{
backgroundColor: "blue",
height: 20,
width: 20,
borderRadius: 10,
},
animatedStyle,
]}
/>
<View
pointerEvents="none"
style={{
backgroundColor: "black",
height: 2,
width: "100%",
position: "absolute",
top: 10,
}}
/>
</Animated.View>
</PanGestureHandler>
</View>
</SafeAreaView>
);
};
If this blog was helpful, check out our full blog archive.