Last active
July 13, 2024 02:53
-
-
Save Aryk/380dffa6464603ec7b1e0c3598edff65 to your computer and use it in GitHub Desktop.
A custom extensible switch built in reanimated v2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {useEffect} from "react"; | |
import {StyleProp, StyleSheet, TouchableOpacity, TouchableOpacityProps, ViewProps, ViewStyle} from "react-native"; | |
import Animated, { | |
interpolate, | |
useAnimatedStyle, | |
useSharedValue, | |
withTiming, | |
Easing, | |
Extrapolate, | |
interpolateColor, AnimateProps, | |
} from "react-native-reanimated"; | |
const AnimatedTouchableOpacity = Animated.createAnimatedComponent<TouchableOpacityProps>(TouchableOpacity); | |
interface ICustomSwitchToggle extends Pick< | |
ICustomSwitch, | |
"toggleColorOff" | | |
"toggleColorOn" | | |
"trackColorOff" | | |
"trackColorOn" | |
> { | |
style?: AnimateProps<ViewProps>["style"]; | |
animatedValue?: Animated.SharedValue<number>; | |
width?: number; | |
height?: number; | |
} | |
const CustomSwitchToggle = ({style}: ICustomSwitchToggle) => <Animated.View style={style} />; | |
// From here: https://nextcodingjs.com/create-custom-toggle-switch-button/, but added: | |
// | |
// 1. Made it a controlled component, so you can turn it off/on from some other place. | |
// 2. Used "onValueChange" which is more in line with react switch | |
// 3. Allow ability to render whatever you want on the inside of the switch, and tap into the animatedValue so you can do opacity animations. | |
// 4. Allow for non-circular toggle. | |
// 5. Fixed a few positioning bugs, so it automatically adapts to varying border widths | |
// 6. Allow for animating the track color | |
// 7. Added typescript types | |
// | |
// Hopefully this is useful, and if you like to build high quality stuff, we're hiring! [email protected]. | |
interface ICustomSwitch { | |
value: boolean; | |
onValueChange: (value: boolean) => any; | |
containerStyle?: StyleProp<ViewStyle>; | |
toggleColorOff?: string; | |
toggleColorOn?: string; | |
trackColorOff?: string; | |
trackColorOn?: string; | |
height?: number; | |
width?: number; | |
padding?: number; | |
borderWidth?: number; | |
borderColor?: string; | |
duration?: number; | |
toggleHeight?: number; | |
toggleWidth?: number; | |
ToggleComponent?: React.ElementType<ICustomSwitchToggle>; | |
} | |
const CustomSwitch = ({ | |
onValueChange, | |
value = false, | |
containerStyle, | |
toggleColorOff = "#c3c3c3", | |
toggleColorOn = "#008ECC", | |
trackColorOff = "#333333", | |
trackColorOn = "#CCCCCC", | |
height = 40, | |
width = height * 1.875, | |
padding = height * 0.09, | |
borderWidth = 1, | |
borderColor = toggleColorOff, | |
duration = 400, | |
toggleHeight = height - (padding + borderWidth) * 2, | |
toggleWidth = toggleHeight, | |
ToggleComponent = CustomSwitchToggle, | |
}: ICustomSwitch) => { | |
const calculateAnimatedValue = (v: boolean) => v ? 1 : 0; | |
const animatedValue = useSharedValue(calculateAnimatedValue(value)); | |
const setAnimatedValue = (newValue: boolean) => { | |
animatedValue.value = withTiming( | |
calculateAnimatedValue(newValue), | |
{ | |
duration, | |
easing: Easing.bezier(0.4, 0.0, 0.2, 1), | |
}, | |
); | |
} | |
useEffect( | |
() => { | |
setAnimatedValue(value); | |
}, | |
[value], | |
); | |
const onPress = () => { | |
const newValue = animatedValue.value === 0; | |
setAnimatedValue(newValue); | |
onValueChange(newValue); | |
}; | |
const switchAnimatedStyle = useAnimatedStyle( | |
() => ({ | |
transform: [ | |
{ | |
translateX: interpolate( | |
animatedValue.value, | |
[0, 1], | |
[padding, width - toggleWidth - padding - 2 * borderWidth], | |
Extrapolate.CLAMP, | |
), | |
}, | |
], | |
backgroundColor: interpolateColor( | |
animatedValue.value, | |
[0, 1], | |
[toggleColorOff, toggleColorOn], | |
), | |
}), | |
[ | |
padding, | |
width, | |
toggleWidth, | |
padding, | |
borderWidth, | |
toggleColorOff, | |
toggleColorOn, | |
] | |
); | |
const containerAnimatedStyle = useAnimatedStyle( | |
() => ({ | |
backgroundColor: interpolateColor( | |
animatedValue.value, | |
[0, 1], | |
[trackColorOff, trackColorOn], | |
), | |
}), | |
[ | |
trackColorOff, | |
trackColorOn, | |
] | |
); | |
return <AnimatedTouchableOpacity | |
onPress={onPress} | |
activeOpacity={1} | |
style={[ | |
styles.containerStyle, | |
{height, width, borderWidth, borderColor}, | |
containerAnimatedStyle, | |
containerStyle | |
]} | |
> | |
<ToggleComponent | |
{...{ | |
toggleColorOff, | |
toggleColorOn, | |
trackColorOff, | |
trackColorOn, | |
animatedValue, | |
}} | |
width={toggleWidth} | |
height={toggleHeight} | |
style={[ | |
styles.switchButton, | |
{left: 0, height: toggleHeight, width: toggleWidth}, | |
switchAnimatedStyle, | |
]} | |
/> | |
</AnimatedTouchableOpacity>; | |
}; | |
const styles = StyleSheet.create({ | |
containerStyle: { | |
justifyContent: 'center', | |
alignItems: 'center', | |
backgroundColor: 'white', | |
borderRadius: 500, | |
}, | |
switchButton: { | |
backgroundColor: 'red', | |
position: 'absolute', | |
borderRadius: 100, | |
}, | |
}); | |
export { | |
ICustomSwitchToggle, | |
CustomSwitchToggle, | |
ICustomSwitch, | |
CustomSwitch, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment