summaryrefslogtreecommitdiff
path: root/app/components/switch/switch.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/components/switch/switch.tsx')
-rw-r--r--app/components/switch/switch.tsx114
1 files changed, 114 insertions, 0 deletions
diff --git a/app/components/switch/switch.tsx b/app/components/switch/switch.tsx
new file mode 100644
index 0000000..0813747
--- /dev/null
+++ b/app/components/switch/switch.tsx
@@ -0,0 +1,114 @@
+import React from "react"
+import { ViewStyle, Animated, Easing, TouchableWithoutFeedback } from "react-native"
+import { color } from "../../theme"
+import { SwitchProps } from "./switch.props"
+
+// dimensions
+const THUMB_SIZE = 30
+const WIDTH = 56
+const MARGIN = 2
+const OFF_POSITION = -0.5
+const ON_POSITION = WIDTH - THUMB_SIZE - MARGIN
+const BORDER_RADIUS = (THUMB_SIZE * 3) / 4
+
+// colors
+const ON_COLOR = color.primary
+const OFF_COLOR = color.palette.offWhite
+const BORDER_ON_COLOR = ON_COLOR
+const BORDER_OFF_COLOR = "rgba(0, 0, 0, 0.1)"
+
+// animation
+const DURATION = 250
+
+// the track always has these props
+const TRACK = {
+ height: THUMB_SIZE + MARGIN,
+ width: WIDTH,
+ borderRadius: BORDER_RADIUS,
+ borderWidth: MARGIN / 2,
+ backgroundColor: color.background,
+}
+
+// the thumb always has these props
+const THUMB: ViewStyle = {
+ position: "absolute",
+ width: THUMB_SIZE,
+ height: THUMB_SIZE,
+ borderColor: BORDER_OFF_COLOR,
+ borderRadius: THUMB_SIZE / 2,
+ borderWidth: MARGIN / 2,
+ backgroundColor: color.background,
+ shadowColor: BORDER_OFF_COLOR,
+ shadowOffset: { width: 1, height: 2 },
+ shadowOpacity: 1,
+ shadowRadius: 2,
+ elevation: 2,
+}
+
+const makeAnimatedValue = (switchOn) => new Animated.Value(switchOn ? 1 : 0)
+
+export function Switch(props: SwitchProps) {
+ const [timer] = React.useState<Animated.Value>(makeAnimatedValue(props.value))
+ const startAnimation = React.useMemo(
+ () => (newValue: boolean) => {
+ const toValue = newValue ? 1 : 0
+ const easing = Easing.out(Easing.circle)
+ Animated.timing(timer, {
+ toValue,
+ duration: DURATION,
+ easing,
+ useNativeDriver: true,
+ }).start()
+ },
+ [timer],
+ )
+
+ const [previousValue, setPreviousValue] = React.useState<boolean>(props.value)
+ React.useEffect(() => {
+ if (props.value !== previousValue) {
+ startAnimation(props.value)
+ setPreviousValue(props.value)
+ }
+ }, [props.value])
+
+ const handlePress = React.useMemo(() => () => props.onToggle && props.onToggle(!props.value), [
+ props.onToggle,
+ props.value,
+ ])
+
+ if (!timer) {
+ return null
+ }
+
+ const translateX = timer.interpolate({
+ inputRange: [0, 1],
+ outputRange: [OFF_POSITION, ON_POSITION],
+ })
+
+ const style = props.style
+
+ const trackStyle = [
+ TRACK,
+ {
+ backgroundColor: props.value ? ON_COLOR : OFF_COLOR,
+ borderColor: props.value ? BORDER_ON_COLOR : BORDER_OFF_COLOR,
+ },
+ props.value ? props.trackOnStyle : props.trackOffStyle,
+ ]
+
+ const thumbStyle = [
+ THUMB,
+ {
+ transform: [{ translateX }],
+ },
+ props.value ? props.thumbOnStyle : props.thumbOffStyle,
+ ]
+
+ return (
+ <TouchableWithoutFeedback onPress={handlePress} style={style}>
+ <Animated.View style={trackStyle}>
+ <Animated.View style={thumbStyle} />
+ </Animated.View>
+ </TouchableWithoutFeedback>
+ )
+}