'Spring animation not working on rerender with interpolate and useDidMountEffect hook on Android and iOS
The rotate animation for the needle currently looks like this:
(no animation)

And my code currently looks like this:
import React, {useRef} from 'react';
import {Animated, Platform, StyleSheet, View} from 'react-native';
import * as Progress from 'react-native-progress';
import MaskedView from '@react-native-masked-view/masked-view';
import {withAnchorPoint} from 'react-native-anchor-point';
import useDidMountEffect from '../helper/useDidMountEffect';
import {LinearGradient} from 'expo-linear-gradient';
const CIRCLE = Math.PI * 2;
const Gauge = (props) => {
const style = StyleSheet.compose(styles.arc, props.style);
const {
size,
progress,
animated,
overallGradient,
addTriangleTip,
triangleTipWidth,
triangleTipHeight,
triangleNeedle,
addCircle,
circleSize,
alwaysUseEndAngle,
endAngle,
unfilledEndAngle,
rotate,
thickness,
borderWidth,
needleWidth,
needleHeight,
needleBorderRadius,
translateNeedleY,
color,
borderColor,
needleColor,
unfilledColor,
circleColor,
triangleTipColor,
} = props;
const circleProgressInDegrees = (progress * CIRCLE * unfilledEndAngle) * 57.29578
const prevProgressRef = useRef();
useDidMountEffect(() => {
if (animated) {
prevProgressRef.current = circleProgressInDegrees;
moveNeedleFn();
}
}, [progress, unfilledEndAngle]);
const needleAnim = useRef(new Animated.Value(0)).current;
const rotateNeedle = needleAnim.interpolate({
inputRange: [0, 1],
outputRange: [`${prevProgressRef.current ? prevProgressRef.current : circleProgressInDegrees}deg`, `${circleProgressInDegrees}deg`]
});
const moveNeedleFn = () => {
Animated.spring(needleAnim, {
toValue: 1,
bounciness: 0,
useNativeDriver: true,
}).start();
};
return (
<View>
{overallGradient ? (
<View
style={{
...styles.maskContainer,
height: size,
width: size,
}}>
<MaskedView
maskElement={
<Progress.Circle
size={size}
progress={progress}
alwaysUseEndAngle={alwaysUseEndAngle}
endAngle={endAngle}
unfilledEndAngle={unfilledEndAngle}
thickness={thickness}
borderWidth={borderWidth}
color={color}
borderColor={borderColor}
unfilledColor={unfilledColor}
indeterminate={false}
style={{...style, transform: [{rotate: rotate}]}}>
<Animated.View
style={[
{
position: 'absolute',
width: triangleNeedle ? 0 : needleWidth,
height: triangleNeedle ? 0 : needleHeight,
top: size / 2 - needleHeight - translateNeedleY / 2,
borderTopWidth: 0,
borderLeftWidth: triangleNeedle ? needleWidth : 0,
borderRightWidth: triangleNeedle ? needleWidth : 0,
borderBottomWidth: triangleNeedle ? needleHeight : 0,
backgroundColor: triangleNeedle
? 'transparent'
: needleColor,
borderRadius: triangleNeedle ? 0 : needleBorderRadius,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: needleColor,
},
withAnchorPoint(
{transform: [{rotate: prevProgressRef.current ? rotateNeedle : `${circleProgressInDegrees}deg`}]},
{x: 0.5, y: 1},
{
width: needleWidth,
height: needleHeight + translateNeedleY,
},
),
]}>
{addTriangleTip && (
<View
style={{
alignSelf: 'center',
top: -triangleTipHeight,
borderTopWidth: 0,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: triangleTipColor,
borderLeftWidth: triangleTipWidth,
borderRightWidth: triangleTipWidth,
borderBottomWidth: triangleTipHeight,
}}
/>
)}
</Animated.View>
{addCircle && (
<Animated.View
style={{
position: 'absolute',
height: circleSize,
width: circleSize,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
transform: [{rotate: prevProgressRef.current ? rotateNeedle : `${circleProgressInDegrees}deg`}],
}}
/>
)}
</Progress.Circle>
}>
<LinearGradient
colors={overallGradient}
style={{
height: size,
width: size,
}}
/>
</MaskedView>
</View>
) : (
<Progress.Circle
size={size}
progress={progress}
alwaysUseEndAngle={alwaysUseEndAngle}
endAngle={endAngle}
unfilledEndAngle={unfilledEndAngle}
thickness={thickness}
borderWidth={borderWidth}
color={color}
borderColor={borderColor}
unfilledColor={unfilledColor}
indeterminate={false}
style={{...style, transform: [{rotate: rotate}]}}>
<Animated.View
style={[
{
position: 'absolute',
width: triangleNeedle ? 0 : needleWidth,
height: triangleNeedle ? 0 : needleHeight,
top: size / 2 - needleHeight - translateNeedleY / 2,
borderTopWidth: 0,
borderLeftWidth: triangleNeedle ? needleWidth : 0,
borderRightWidth: triangleNeedle ? needleWidth : 0,
borderBottomWidth: triangleNeedle ? needleHeight : 0,
backgroundColor: triangleNeedle ? 'transparent' : needleColor,
borderRadius: triangleNeedle ? 0 : needleBorderRadius,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: needleColor,
},
withAnchorPoint(
{transform: [{rotate: prevProgressRef.current ? rotateNeedle : `${circleProgressInDegrees}deg`}]},
{x: 0.5, y: 1},
{
width: needleWidth,
height: needleHeight + translateNeedleY,
},
),
]}>
{addTriangleTip && (
<View
style={{
alignSelf: 'center',
top: -triangleTipHeight,
borderTopWidth: 0,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: triangleTipColor,
borderLeftWidth: triangleTipWidth,
borderRightWidth: triangleTipWidth,
borderBottomWidth: triangleTipHeight,
}}
/>
)}
</Animated.View>
{addCircle && (
<Animated.View
style={{
position: 'absolute',
height: circleSize,
width: circleSize,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
transform: [{rotate: prevProgressRef.current ? rotateNeedle : `${circleProgressInDegrees}deg`}],
}}
/>
)}
</Progress.Circle>
)}
</View>
);
};
Gauge.defaultProps = {
size: 30,
progress: 0.5,
overallGradient: false,
addTriangleTip: false,
triangleTipWidth: 2,
triangleTipHeight: 4,
triangleNeedle: false,
addCircle: false,
circleSize: 15,
animated: true,
alwaysUseEndAngle: true,
endAngle: 0.9,
unfilledEndAngle: 0.9,
rotate: '-90deg',
thickness: 6,
borderWidth: 1,
needleWidth: 2,
needleHeight: 45,
needleBorderRadius: 0,
translateNeedleY: 0,
color: 'blue',
borderColor: 'blue',
needleColor: 'blue',
unfilledColor: 'grey',
circleColor: 'blue',
triangleTipColor: 'blue',
};
let styles;
if (Platform.OS === 'ios') {
styles = StyleSheet.create({
arc: {
alignItems: 'center',
justifyContent: 'center',
},
maskContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});
} else {
styles = StyleSheet.create({
arc: {
alignItems: 'center',
justifyContent: 'center',
},
maskContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});
}
export default Gauge;
I'm confused since my interpolate() and Animated.spring() are setup correctly. As you can see it even rotates the needle with every change to my progress or unfilledEndAngle dependencies because of the useDidMountEffect hook.
I had to change to this format of using interpolate and degrees for my component to be compatible across iOS and Android. Otherwise, I had this simpler version where the spring animation worked on every rerender, but only worked on iOS:
import React, {useRef} from 'react';
import {Animated, Platform, StyleSheet, View} from 'react-native';
import * as Progress from 'react-native-progress';
import MaskedView from '@react-native-masked-view/masked-view';
import {withAnchorPoint} from 'react-native-anchor-point';
import useDidMountEffect from '../helper/useDidMountEffect';
import {LinearGradient} from 'expo-linear-gradient';
const CIRCLE = Math.PI * 2;
const Gauge = (props) => {
const style = StyleSheet.compose(styles.arc, props.style);
const {
size,
progress,
animated,
overallGradient,
addTriangleTip,
triangleTipWidth,
triangleTipHeight,
triangleNeedle,
addCircle,
circleSize,
alwaysUseEndAngle,
endAngle,
unfilledEndAngle,
rotate,
thickness,
borderWidth,
needleWidth,
needleHeight,
needleBorderRadius,
translateNeedleY,
color,
borderColor,
needleColor,
unfilledColor,
circleColor,
triangleTipColor,
} = props;
useDidMountEffect(() => {
if (animated) {
moveNeedleFn();
}
}, [progress, unfilledEndAngle]);
const moveNeedle = useRef(
new Animated.Value(progress * CIRCLE * unfilledEndAngle),
).current;
const moveNeedleFn = () => {
Animated.spring(moveNeedle, {
toValue: progress * CIRCLE * unfilledEndAngle,
bounciness: 0,
useNativeDriver: true,
}).start();
};
return (
<View>
{overallGradient ? (
<View
style={{
...styles.maskContainer,
height: size,
width: size,
}}>
<MaskedView
maskElement={
<Progress.Circle
size={size}
progress={progress}
alwaysUseEndAngle={alwaysUseEndAngle}
endAngle={endAngle}
unfilledEndAngle={unfilledEndAngle}
thickness={thickness}
borderWidth={borderWidth}
color={color}
borderColor={borderColor}
unfilledColor={unfilledColor}
indeterminate={false}
style={{...style, transform: [{rotate: rotate}]}}>
<Animated.View
style={[
{
position: 'absolute',
width: triangleNeedle ? 0 : needleWidth,
height: triangleNeedle ? 0 : needleHeight,
top: size / 2 - needleHeight - translateNeedleY / 2,
borderTopWidth: 0,
borderLeftWidth: triangleNeedle ? needleWidth : 0,
borderRightWidth: triangleNeedle ? needleWidth : 0,
borderBottomWidth: triangleNeedle ? needleHeight : 0,
backgroundColor: triangleNeedle
? 'transparent'
: needleColor,
borderRadius: triangleNeedle ? 0 : needleBorderRadius,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: needleColor,
},
withAnchorPoint(
{transform: [{rotateZ: moveNeedle}]},
{x: 0.5, y: 1},
{
width: needleWidth,
height: needleHeight + translateNeedleY,
},
),
]}>
{addTriangleTip && (
<View
style={{
alignSelf: 'center',
top: -triangleTipHeight,
borderTopWidth: 0,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: triangleTipColor,
borderLeftWidth: triangleTipWidth,
borderRightWidth: triangleTipWidth,
borderBottomWidth: triangleTipHeight,
}}
/>
)}
</Animated.View>
{addCircle && (
<Animated.View
style={{
position: 'absolute',
height: circleSize,
width: circleSize,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
transform: [{rotateZ: moveNeedle}],
}}
/>
)}
</Progress.Circle>
}>
<LinearGradient
colors={overallGradient}
style={{
height: size,
width: size,
}}
/>
</MaskedView>
</View>
) : (
<Progress.Circle
size={size}
progress={progress}
alwaysUseEndAngle={alwaysUseEndAngle}
endAngle={endAngle}
unfilledEndAngle={unfilledEndAngle}
thickness={thickness}
borderWidth={borderWidth}
color={color}
borderColor={borderColor}
unfilledColor={unfilledColor}
indeterminate={false}
style={{...style, transform: [{rotate: rotate}]}}>
<Animated.View
style={[
{
position: 'absolute',
width: triangleNeedle ? 0 : needleWidth,
height: triangleNeedle ? 0 : needleHeight,
top: size / 2 - needleHeight - translateNeedleY / 2,
borderTopWidth: 0,
borderLeftWidth: triangleNeedle ? needleWidth : 0,
borderRightWidth: triangleNeedle ? needleWidth : 0,
borderBottomWidth: triangleNeedle ? needleHeight : 0,
backgroundColor: triangleNeedle ? 'transparent' : needleColor,
borderRadius: triangleNeedle ? 0 : needleBorderRadius,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: needleColor,
},
withAnchorPoint(
{transform: [{rotateZ: moveNeedle}]},
{x: 0.5, y: 1},
{
width: needleWidth,
height: needleHeight + translateNeedleY,
},
),
]}>
{addTriangleTip && (
<View
style={{
alignSelf: 'center',
top: -triangleTipHeight,
borderTopWidth: 0,
borderStyle: 'solid',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: triangleTipColor,
borderLeftWidth: triangleTipWidth,
borderRightWidth: triangleTipWidth,
borderBottomWidth: triangleTipHeight,
}}
/>
)}
</Animated.View>
{addCircle && (
<Animated.View
style={{
position: 'absolute',
height: circleSize,
width: circleSize,
borderRadius: circleSize / 2,
backgroundColor: circleColor,
transform: [{rotateZ: moveNeedle}],
}}
/>
)}
</Progress.Circle>
)}
</View>
);
};
Gauge.defaultProps = {
size: 30,
progress: 0.5,
overallGradient: false,
addTriangleTip: false,
triangleTipWidth: 2,
triangleTipHeight: 4,
triangleNeedle: false,
addCircle: false,
circleSize: 15,
animated: true,
alwaysUseEndAngle: true,
endAngle: 0.9,
unfilledEndAngle: 0.9,
rotate: '-90deg',
thickness: 6,
borderWidth: 1,
needleWidth: 2,
needleHeight: 45,
needleBorderRadius: 0,
translateNeedleY: 0,
color: 'blue',
borderColor: 'blue',
needleColor: 'blue',
unfilledColor: 'grey',
circleColor: 'blue',
triangleTipColor: 'blue',
};
let styles;
if (Platform.OS === 'ios') {
styles = StyleSheet.create({
arc: {
alignItems: 'center',
justifyContent: 'center',
},
maskContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});
} else {
styles = StyleSheet.create({
arc: {
alignItems: 'center',
justifyContent: 'center',
},
maskContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});
}
export default Gauge;
And looked like this:

I've also checked and this component renders just once with every change to progress or unfilledEndAngle. I'm wondering if I'm setting this up wrong somehow or maybe there's something wrong internally?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
