possible slider android crash fix #12

merged
opened by whey.party targeting main

sorry react native reanimated is scary stuff

Changed files
+52 -174
src
components
forms
screens
+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
··· 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>