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.
Building gesture handler to detect the slider events
1const [width, setWidth] = useState(200); 2const x = useSharedValue(0); 3 4const gestureHandler = useAnimatedGestureHandler({ 5 onStart: (event, ctx) => { 6 x.value = event.absoluteX; 7 }, 8 onActive: (event, ctx) => { 9 x.value = event.absoluteX; 10 // We need to calculate `progress` here based on the slider position. 11 }, 12}); 13 14const animatedStyle = useAnimatedStyle(() => { 15 return { 16 transform: [ 17 { 18 translateX: x.value, 19 }, 20 ], 21 }; 22}); 23 24return ( 25 <PanGestureHandler onGestureEvent={gestureHandler}> 26 <Animated.View style={[{ height: 20 }]}> 27 <Animated.View 28 pointerEvents="none" 29 style={[ 30 { 31 backgroundColor: "blue", 32 height: 20, 33 width: 20, 34 borderRadius: 10, 35 }, 36 animatedStyle, 37 ]} 38 /> 39 <View 40 pointerEvents="none" 41 style={{ 42 backgroundColor: "black", 43 height: 2, 44 width: "100%", 45 position: "absolute", 46 top: 10, 47 }} 48 /> 49 </Animated.View> 50 </PanGestureHandler> 51);
Loader component:
1<View style={{ height: 20, marginTop: 10 }}> 2 <Lottie 3 style={{ 4 alignSelf: "center", 5 width: "100%", 6 }} 7 progress={"Calculated `progress` based on slider position."} 8 source={require("./progress")} 9 autoPlay={false} 10 loop={false} 11 /> 12</View>
Animate the loader based on the slider position
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.
1const [progress, setProgress] = useState(0); 2const gestureHandler = useAnimatedGestureHandler({ 3 onStart: (event, ctx) => { 4 x.value = event.absoluteX; 5 }, 6 onActive: (event, ctx) => { 7 x.value = event.absoluteX; 8 runOnJS(setProgress)(x.value / width); 9 }, 10}); 11 12<Lottie 13 style={{ 14 alignSelf: "center", 15 width: "100%", 16 }} 17 progress={progress} 18 source={require("./progress")} 19 autoPlay={false} 20 loop={false} 21/>;
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.
1const LottieAnimated = Animated.createAnimatedComponent(Lottie); 2const [progress, setProgress] = useState(0); 3const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width); 4const gestureHandler = useAnimatedGestureHandler({ 5 onStart: (event, ctx) => { 6 x.value = event.absoluteX; 7 }, 8 onActive: (event, ctx) => { 9 x.value = event.absoluteX; 10 }, 11}); 12 13<LottieAnimated 14 style={{ 15 alignSelf: "center", 16 width: "100%", 17 }} 18 animatedProps={lottieAnimatedProps} 19 source={require("./progress")} 20 autoPlay={false} 21 loop={false} 22/>;
Conclusion
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:
1let approach1ReRenderCount1 = 0; 2const Approach1: () => Node = () => { 3 const [width, setWidth] = useState(1); 4 const x = useSharedValue(0); 5 6 const [progress, setProgress] = useState(0); 7 const gestureHandler = useAnimatedGestureHandler({ 8 onStart: (event, ctx) => { 9 x.value = event.absoluteX; 10 }, 11 onActive: (event, ctx) => { 12 x.value = event.absoluteX; 13 runOnJS(setProgress)(x.value / width); 14 }, 15 }); 16 17 const animatedStyle = useAnimatedStyle(() => { 18 return { 19 transform: [ 20 { 21 translateX: x.value, 22 }, 23 ], 24 }; 25 }); 26 27 return ( 28 <SafeAreaView> 29 <View 30 style={{ 31 height: 150, 32 paddingHorizontal: 20, 33 borderWidth: 1, 34 margin: 10, 35 justifyContent: "center", 36 }} 37 onLayout={({ 38 nativeEvent: { 39 layout: { width }, 40 }, 41 }) => { 42 setWidth(width); 43 }} 44 > 45 <Text style={{ fontSize: 20, paddingBottom: 10 }}>Approach 1</Text> 46 <Text style={{ fontSize: 20 }}> 47 Rerender Count : {approach1ReRenderCount1++} 48 </Text> 49 50 <View style={{ height: 20, marginTop: 10 }}> 51 <LottieAnimated 52 style={{ 53 alignSelf: "center", 54 width: "100%", 55 }} 56 progress={progress} 57 source={require("./progress")} 58 autoPlay={false} 59 loop={false} 60 /> 61 </View> 62 <PanGestureHandler onGestureEvent={gestureHandler}> 63 <Animated.View style={[{ height: 20 }]}> 64 <Animated.View 65 style={[ 66 { 67 backgroundColor: "blue", 68 height: 20, 69 width: 20, 70 borderRadius: 10, 71 }, 72 animatedStyle, 73 ]} 74 /> 75 <View 76 style={{ 77 backgroundColor: "black", 78 height: 2, 79 width: "100%", 80 position: "absolute", 81 top: 10, 82 }} 83 /> 84 </Animated.View> 85 </PanGestureHandler> 86 </View> 87 </SafeAreaView> 88 ); 89}; 90 91const LottieAnimated = Animated.createAnimatedComponent(Lottie); 92let approach2ReRenderCount = 0; 93const Approach2: () => Node = () => { 94 const [width, setWidth] = useState(1); 95 const x = useSharedValue(0); 96 97 const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width); 98 99 const gestureHandler = useAnimatedGestureHandler({ 100 onStart: (event, ctx) => { 101 x.value = event.absoluteX; 102 }, 103 onActive: (event, ctx) => { 104 x.value = event.absoluteX; 105 }, 106 }); 107 108 const animatedStyle = useAnimatedStyle(() => { 109 return { 110 transform: [ 111 { 112 translateX: x.value, 113 }, 114 ], 115 }; 116 }); 117 118 return ( 119 <SafeAreaView> 120 <View 121 style={{ 122 height: 150, 123 paddingHorizontal: 20, 124 borderWidth: 1, 125 margin: 10, 126 justifyContent: "center", 127 }} 128 onLayout={({ 129 nativeEvent: { 130 layout: { width }, 131 }, 132 }) => { 133 setWidth(width); 134 }} 135 > 136 <Text style={{ fontSize: 20, paddingBottom: 10 }}> Approach 2</Text> 137 <Text style={{ fontSize: 20 }}> 138 Rerender Count : {approach2ReRenderCount++} 139 </Text> 140 <View style={{ height: 20, marginTop: 10 }}> 141 <LottieAnimated 142 animatedProps={lottieAnimatedProps} 143 style={{ 144 alignSelf: "center", 145 width: "100%", 146 }} 147 source={require("./progress")} 148 autoPlay={false} 149 loop={false} 150 /> 151 </View> 152 <PanGestureHandler onGestureEvent={gestureHandler}> 153 <Animated.View style={[{ height: 20 }]}> 154 <Animated.View 155 pointerEvents="none" 156 style={[ 157 { 158 backgroundColor: "blue", 159 height: 20, 160 width: 20, 161 borderRadius: 10, 162 }, 163 animatedStyle, 164 ]} 165 /> 166 <View 167 pointerEvents="none" 168 style={{ 169 backgroundColor: "black", 170 height: 2, 171 width: "100%", 172 position: "absolute", 173 top: 10, 174 }} 175 /> 176 </Animated.View> 177 </PanGestureHandler> 178 </View> 179 </SafeAreaView> 180 ); 181};