feat: react native community slider

This commit is contained in:
leon
2026-03-04 13:40:15 +01:00
parent 372b59660f
commit d4fd96ce03
4 changed files with 2155 additions and 26687 deletions
-3447
View File
File diff suppressed because it is too large Load Diff
-21106
View File
File diff suppressed because it is too large Load Diff
+91 -97
View File
@@ -1,12 +1,14 @@
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
import NativeSlider from '@react-native-community/slider'
import { Palette, Style } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { LinearGradient } from './LinearGradient/LinearGradient'
const INITIAL_BOX_SIZE = 6
const TRACK_HEIGHT = 6
const HANDLE_SIZE = 20
const ACTIVE_TRACK_MIN_WIDTH = 6
const clamp01 = (value) => Math.min(1, Math.max(0, Number(value) || 0))
export default ({
value,
@@ -17,116 +19,94 @@ export default ({
onSeekEnd,
seekEnabled = false,
}) => {
const offset = useSharedValue(0)
const boxWidth = useSharedValue(INITIAL_BOX_SIZE)
const [sliderWidth, setSliderWidth] = useState(0)
const [layoutWidth, setLayoutWidth] = useState(0)
const [sliderValue, setSliderValue] = useState(clamp01(progress))
const seekingRef = useRef(false)
const MAX_VALUE = Math.max(0, sliderWidth - INITIAL_BOX_SIZE)
// Keep slider synced with external progress except while user is dragging.
useEffect(() => {
if (seekingRef.current) return
if (typeof progress === 'number') {
setSliderValue(clamp01(progress))
}
}, [progress])
const handleSeekStart = () => {
if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) {
const handleSlidingStart = useCallback(() => {
if (!seekEnabled || seekingRef.current) return
seekingRef.current = true
if (typeof onSeekStart === 'function') {
onSeekStart()
}
}
}, [onSeekStart, seekEnabled])
const handleSeekEnd = () => {
if (seekingRef.current && typeof onSeekEnd === 'function') {
const handleValueChange = useCallback(
(nextValue) => {
if (!seekEnabled) return
setSliderValue(clamp01(nextValue))
},
[seekEnabled]
)
const handleSlidingComplete = useCallback(
(nextValue) => {
const ratio = clamp01(nextValue)
setSliderValue(ratio)
if (seekEnabled && typeof onSeek === 'function') {
onSeek(ratio)
}
if (seekingRef.current) {
seekingRef.current = false
if (typeof onSeekEnd === 'function') {
onSeekEnd()
}
}
},
[onSeek, onSeekEnd, seekEnabled]
)
const pan = Gesture.Pan()
.enabled(seekEnabled)
.onBegin(() => {
runOnJS(handleSeekStart)()
})
.onStart(() => {
runOnJS(handleSeekStart)()
})
.onChange((event) => {
runOnJS(handleSeekStart)()
if (!(MAX_VALUE > 0)) {
offset.value = 0
boxWidth.value = INITIAL_BOX_SIZE
return
}
offset.value =
Math.abs(offset.value) <= MAX_VALUE
? offset.value + event.changeX <= 0
? 0
: offset.value + event.changeX >= MAX_VALUE
? MAX_VALUE
: offset.value + event.changeX
: offset.value
const handleLayout = useCallback((event) => {
const width = Math.max(0, Math.round(event?.nativeEvent?.layout?.width || 0))
setLayoutWidth((prev) => (prev === width ? prev : width))
}, [])
const newWidth = INITIAL_BOX_SIZE + offset.value
boxWidth.value = newWidth
})
.onEnd(() => {
if (seekEnabled && typeof onSeek === 'function' && MAX_VALUE > 0) {
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0
// Reanimated -> JS thread bridge
runOnJS(onSeek)(ratio)
}
runOnJS(handleSeekEnd)()
})
.onFinalize(() => {
runOnJS(handleSeekEnd)()
})
// Reflect external progress into the slider UI
useEffect(() => {
if (seekingRef.current) return
if (typeof progress === 'number' && sliderWidth > 0) {
const max = sliderWidth - INITIAL_BOX_SIZE
if (!(max > 0)) {
offset.value = 0
boxWidth.value = INITIAL_BOX_SIZE
return
}
const clamped = Math.max(0, Math.min(1, progress))
const newOffset = clamped * max
offset.value = newOffset
boxWidth.value = INITIAL_BOX_SIZE + newOffset
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress, sliderWidth])
const boxStyle = useAnimatedStyle(() => {
return {
width: INITIAL_BOX_SIZE + offset.value,
}
})
const sliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
}
})
const visualMetrics = useMemo(() => {
const ratio = clamp01(sliderValue)
const fillWidth =
layoutWidth > 0 ? Math.max(ACTIVE_TRACK_MIN_WIDTH, layoutWidth * ratio) : ACTIVE_TRACK_MIN_WIDTH
const handleLeft = layoutWidth > 0 ? ratio * (layoutWidth - HANDLE_SIZE) : 0
return { fillWidth, handleLeft }
}, [layoutWidth, sliderValue])
console.log("using react native slider ")
return (
<View
onLayout={(e) => {
const nextWidth = Math.max(0, Math.round(e.nativeEvent.layout?.width || 0))
setSliderWidth((prev) => (prev === nextWidth ? prev : nextWidth))
}}
>
<View style={{ ...styles.sliderTrack, width: sliderWidth || undefined }}>
<Animated.View style={[styles.box, boxStyle]}>
<View>
<View style={styles.trackArea} onLayout={handleLayout}>
<View style={styles.sliderTrack}>
<View style={[styles.box, { width: visualMetrics.fillWidth }]}>
<LinearGradient
colors={['#F94697', '#7023F7']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={{ flex: 1, borderRadius: 20 }}
/>
</Animated.View>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.sliderHandle, sliderStyle]} />
</GestureDetector>
</View>
</View>
<NativeSlider
value={sliderValue}
minimumValue={0}
maximumValue={1}
step={0}
disabled={!seekEnabled}
onSlidingStart={handleSlidingStart}
onValueChange={handleValueChange}
onSlidingComplete={handleSlidingComplete}
minimumTrackTintColor="transparent"
maximumTrackTintColor="transparent"
thumbTintColor="transparent"
tapToSeek
style={styles.nativeSlider}
/>
<View pointerEvents="none" style={[styles.sliderHandle, { left: visualMetrics.handleLeft }]} />
</View>
<View style={{ ...Style.containerSpaceBetween, marginTop: 10 }}>
<Text style={styles.time}>{value}</Text>
@@ -137,15 +117,22 @@ export default ({
}
const styles = StyleSheet.create({
trackArea: {
width: '100%',
height: HANDLE_SIZE,
justifyContent: 'center',
position: 'relative',
},
box: {
height: INITIAL_BOX_SIZE,
height: TRACK_HEIGHT,
borderRadius: 20,
position: 'absolute',
zIndex: 1,
overflow: 'hidden',
},
sliderHandle: {
width: 20,
height: 20,
width: HANDLE_SIZE,
height: HANDLE_SIZE,
backgroundColor: '#f8f9ff',
borderRadius: 25,
position: 'absolute',
@@ -161,8 +148,15 @@ const styles = StyleSheet.create({
shadowRadius: 3.05,
elevation: 4,
},
nativeSlider: {
position: 'absolute',
left: 0,
right: 0,
height: HANDLE_SIZE,
zIndex: 3,
},
sliderTrack: {
height: 6,
height: TRACK_HEIGHT,
backgroundColor: '#0F0C19',
borderRadius: 25,
justifyContent: 'center',
+2053 -2026
View File
File diff suppressed because it is too large Load Diff