sorry react native reanimated is scary stuff
+50
-170
src/components/forms/Slider.tsx
+50
-170
src/components/forms/Slider.tsx
···
1
-
import {useCallback, useEffect, useRef, useState} from 'react'
2
-
import {type StyleProp, View, type ViewStyle} from 'react-native'
3
-
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
4
-
import Animated, {
5
-
runOnJS,
6
-
useAnimatedStyle,
7
-
useSharedValue,
8
-
withSpring,
9
-
} from 'react-native-reanimated'
1
+
import {type ViewStyle} from 'react-native'
2
+
import {Slider as RNSlider} from '@miblanchard/react-native-slider'
10
3
11
-
import {useHaptics} from '#/lib/haptics'
12
-
import {atoms as a, platform, useTheme} from '#/alf'
4
+
import {useTheme} from '#/alf'
13
5
14
-
export interface SliderProps {
6
+
interface SliderProps {
15
7
value: number
16
8
onValueChange: (value: number) => void
17
-
min?: number
18
-
max?: number
9
+
minimumValue?: number
10
+
maximumValue?: number
19
11
step?: number
20
-
label?: string
21
-
accessibilityHint?: string
22
-
style?: StyleProp<ViewStyle>
23
-
debounce?: number
12
+
trackStyle?: ViewStyle
13
+
minimumTrackStyle?: ViewStyle
14
+
thumbStyle?: ViewStyle
15
+
thumbTouchSize?: {width: number; height: number}
24
16
}
25
17
26
18
export function Slider({
27
19
value,
28
20
onValueChange,
29
-
min = 0,
30
-
max = 100,
21
+
minimumValue = 0,
22
+
maximumValue = 1,
31
23
step = 1,
32
-
label,
33
-
accessibilityHint,
34
-
style,
35
-
debounce,
24
+
trackStyle,
25
+
minimumTrackStyle,
26
+
thumbStyle,
27
+
thumbTouchSize = {width: 40, height: 40},
36
28
}: SliderProps) {
37
29
const t = useTheme()
38
-
const playHaptic = useHaptics()
39
-
const timerRef = useRef<NodeJS.Timeout | undefined>(undefined)
40
-
41
-
const [width, setWidth] = useState(0)
42
-
43
-
const progress = useSharedValue(0)
44
-
const isPressed = useSharedValue(false)
45
-
46
-
useEffect(() => {
47
-
if (!isPressed.value) {
48
-
const clamped = Math.min(Math.max(value, min), max)
49
-
const normalized = (clamped - min) / (max - min)
50
-
progress.value = withSpring(normalized, {overshootClamping: true})
51
-
}
52
-
}, [value, min, max, progress, isPressed])
53
-
54
-
useEffect(() => {
55
-
return () => {
56
-
if (timerRef.current) clearTimeout(timerRef.current)
57
-
}
58
-
}, [])
59
-
60
-
const updateValueJS = useCallback(
61
-
(val: number) => {
62
-
if (debounce && debounce > 0) {
63
-
if (timerRef.current) {
64
-
clearTimeout(timerRef.current)
65
-
}
66
-
timerRef.current = setTimeout(() => {
67
-
onValueChange(val)
68
-
}, debounce)
69
-
} else {
70
-
onValueChange(val)
71
-
}
72
-
},
73
-
[onValueChange, debounce],
74
-
)
75
-
76
-
const handleValueChange = useCallback(
77
-
(newProgress: number) => {
78
-
'worklet'
79
-
const rawValue = min + newProgress * (max - min)
80
-
81
-
const steppedValue = Math.round(rawValue / step) * step
82
-
const clamped = Math.min(Math.max(steppedValue, min), max)
83
-
84
-
runOnJS(updateValueJS)(clamped)
85
-
},
86
-
[min, max, step, updateValueJS],
87
-
)
88
-
89
-
const pan = Gesture.Pan()
90
-
.onBegin(e => {
91
-
isPressed.value = true
92
-
93
-
if (width > 0) {
94
-
const newProgress = Math.min(Math.max(e.x / width, 0), 1)
95
-
progress.value = newProgress
96
-
handleValueChange(newProgress)
97
-
}
98
-
})
99
-
.onUpdate(e => {
100
-
if (width === 0) return
101
-
const newProgress = Math.min(Math.max(e.x / width, 0), 1)
102
-
progress.value = newProgress
103
-
handleValueChange(newProgress)
104
-
})
105
-
.onFinalize(() => {
106
-
isPressed.value = false
107
-
runOnJS(playHaptic)('Light')
108
-
})
109
-
110
-
const thumbAnimatedStyle = useAnimatedStyle(() => {
111
-
const translateX = progress.value * width
112
-
return {
113
-
transform: [
114
-
{translateX: translateX - 12},
115
-
{scale: isPressed.value ? 1.1 : 1},
116
-
],
117
-
}
118
-
})
119
-
120
-
const trackAnimatedStyle = useAnimatedStyle(() => {
121
-
return {
122
-
width: `${progress.value * 100}%`,
123
-
}
124
-
})
125
30
126
31
return (
127
-
<View
128
-
style={[a.w_full, a.justify_center, {height: 28}, style]}
129
-
accessibilityRole="adjustable"
130
-
accessibilityLabel={label}
131
-
accessibilityHint={accessibilityHint}
132
-
accessibilityValue={{min, max, now: value}}>
133
-
<GestureDetector gesture={pan}>
134
-
<View
135
-
style={[a.flex_1, a.justify_center, {cursor: 'pointer'}]}
136
-
// @ts-ignore web-only style
137
-
onLayout={e => setWidth(e.nativeEvent.layout.width)}>
138
-
<View
139
-
style={[
140
-
a.w_full,
141
-
a.absolute,
142
-
t.atoms.bg_contrast_50,
143
-
{height: 4, borderRadius: 2},
144
-
]}
145
-
/>
146
-
147
-
<Animated.View
148
-
style={[
149
-
a.absolute,
150
-
a.rounded_full,
151
-
{height: 4, backgroundColor: t.palette.primary_500},
152
-
trackAnimatedStyle,
153
-
]}
154
-
/>
155
-
156
-
<Animated.View
157
-
style={[
158
-
a.absolute,
159
-
a.rounded_full,
160
-
t.atoms.bg,
161
-
{
162
-
left: 0,
163
-
width: 24,
164
-
height: 24,
165
-
borderWidth: 1,
166
-
borderColor: t.atoms.border_contrast_low.borderColor,
167
-
},
168
-
thumbAnimatedStyle,
169
-
platform({
170
-
web: {
171
-
boxShadow: '0px 2px 4px 0px #0000001A',
172
-
},
173
-
ios: {
174
-
shadowColor: '#000',
175
-
shadowOffset: {width: 0, height: 2},
176
-
shadowOpacity: 0.15,
177
-
shadowRadius: 4,
178
-
},
179
-
android: {elevation: 3},
180
-
}),
181
-
]}
182
-
/>
183
-
</View>
184
-
</GestureDetector>
185
-
</View>
32
+
<RNSlider
33
+
value={[value]} // always an array
34
+
onValueChange={values => onValueChange(values[0])}
35
+
minimumValue={minimumValue}
36
+
maximumValue={maximumValue}
37
+
step={step}
38
+
trackStyle={{
39
+
height: 4,
40
+
borderRadius: 2,
41
+
backgroundColor: t.atoms.bg_contrast_50.backgroundColor,
42
+
...trackStyle,
43
+
}}
44
+
minimumTrackStyle={{
45
+
height: 4,
46
+
borderRadius: 2,
47
+
backgroundColor: t.palette.primary_500,
48
+
...minimumTrackStyle,
49
+
}}
50
+
thumbStyle={{
51
+
width: 24,
52
+
height: 24,
53
+
borderRadius: 12,
54
+
borderWidth: 1,
55
+
borderColor: t.atoms.border_contrast_low.borderColor,
56
+
backgroundColor: t.atoms.bg.backgroundColor,
57
+
shadowColor: '#000',
58
+
shadowOffset: {width: 0, height: 2},
59
+
shadowOpacity: 0.15,
60
+
shadowRadius: 4,
61
+
elevation: 3,
62
+
...thumbStyle,
63
+
}}
64
+
thumbTouchSize={thumbTouchSize}
65
+
/>
186
66
)
187
67
}
+2
-4
src/screens/Settings/AppearanceSettings.tsx
+2
-4
src/screens/Settings/AppearanceSettings.tsx
···
184
184
<Trans>Hue shift the colors:</Trans>
185
185
</Text>
186
186
<Slider
187
-
label="Volume"
188
187
value={hue}
189
188
onValueChange={setHue}
190
-
min={0}
191
-
max={360}
189
+
minimumValue={0}
190
+
maximumValue={360}
192
191
step={1}
193
-
debounce={0.3}
194
192
/>
195
193
</View>
196
194
</SettingsList.Group>