feat: react native community slider
This commit is contained in:
Generated
-21106
File diff suppressed because it is too large
Load Diff
+91
-97
@@ -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 { StyleSheet, Text, View } from 'react-native'
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
import NativeSlider from '@react-native-community/slider'
|
||||||
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
|
||||||
import { Palette, Style } from '../styles'
|
import { Palette, Style } from '../styles'
|
||||||
import { FONT_FAMILY } from '../styles/Fonts'
|
import { FONT_FAMILY } from '../styles/Fonts'
|
||||||
import { LinearGradient } from './LinearGradient/LinearGradient'
|
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 ({
|
export default ({
|
||||||
value,
|
value,
|
||||||
@@ -17,116 +19,94 @@ export default ({
|
|||||||
onSeekEnd,
|
onSeekEnd,
|
||||||
seekEnabled = false,
|
seekEnabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const offset = useSharedValue(0)
|
const [layoutWidth, setLayoutWidth] = useState(0)
|
||||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE)
|
const [sliderValue, setSliderValue] = useState(clamp01(progress))
|
||||||
const [sliderWidth, setSliderWidth] = useState(0)
|
|
||||||
const seekingRef = useRef(false)
|
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 = () => {
|
const handleSlidingStart = useCallback(() => {
|
||||||
if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) {
|
if (!seekEnabled || seekingRef.current) return
|
||||||
seekingRef.current = true
|
seekingRef.current = true
|
||||||
|
if (typeof onSeekStart === 'function') {
|
||||||
onSeekStart()
|
onSeekStart()
|
||||||
}
|
}
|
||||||
}
|
}, [onSeekStart, seekEnabled])
|
||||||
|
|
||||||
const handleSeekEnd = () => {
|
const handleValueChange = useCallback(
|
||||||
if (seekingRef.current && typeof onSeekEnd === 'function') {
|
(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
|
seekingRef.current = false
|
||||||
|
if (typeof onSeekEnd === 'function') {
|
||||||
onSeekEnd()
|
onSeekEnd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
[onSeek, onSeekEnd, seekEnabled]
|
||||||
|
)
|
||||||
|
|
||||||
const pan = Gesture.Pan()
|
const handleLayout = useCallback((event) => {
|
||||||
.enabled(seekEnabled)
|
const width = Math.max(0, Math.round(event?.nativeEvent?.layout?.width || 0))
|
||||||
.onBegin(() => {
|
setLayoutWidth((prev) => (prev === width ? prev : width))
|
||||||
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 newWidth = INITIAL_BOX_SIZE + offset.value
|
const visualMetrics = useMemo(() => {
|
||||||
boxWidth.value = newWidth
|
const ratio = clamp01(sliderValue)
|
||||||
})
|
const fillWidth =
|
||||||
.onEnd(() => {
|
layoutWidth > 0 ? Math.max(ACTIVE_TRACK_MIN_WIDTH, layoutWidth * ratio) : ACTIVE_TRACK_MIN_WIDTH
|
||||||
if (seekEnabled && typeof onSeek === 'function' && MAX_VALUE > 0) {
|
const handleLeft = layoutWidth > 0 ? ratio * (layoutWidth - HANDLE_SIZE) : 0
|
||||||
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0
|
return { fillWidth, handleLeft }
|
||||||
// Reanimated -> JS thread bridge
|
}, [layoutWidth, sliderValue])
|
||||||
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 }],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
console.log("using react native slider ")
|
||||||
return (
|
return (
|
||||||
<View
|
<View>
|
||||||
onLayout={(e) => {
|
<View style={styles.trackArea} onLayout={handleLayout}>
|
||||||
const nextWidth = Math.max(0, Math.round(e.nativeEvent.layout?.width || 0))
|
<View style={styles.sliderTrack}>
|
||||||
setSliderWidth((prev) => (prev === nextWidth ? prev : nextWidth))
|
<View style={[styles.box, { width: visualMetrics.fillWidth }]}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={{ ...styles.sliderTrack, width: sliderWidth || undefined }}>
|
|
||||||
<Animated.View style={[styles.box, boxStyle]}>
|
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#F94697', '#7023F7']}
|
colors={['#F94697', '#7023F7']}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
style={{ flex: 1, borderRadius: 20 }}
|
style={{ flex: 1, borderRadius: 20 }}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
<GestureDetector gesture={pan}>
|
</View>
|
||||||
<Animated.View style={[styles.sliderHandle, sliderStyle]} />
|
<NativeSlider
|
||||||
</GestureDetector>
|
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>
|
||||||
<View style={{ ...Style.containerSpaceBetween, marginTop: 10 }}>
|
<View style={{ ...Style.containerSpaceBetween, marginTop: 10 }}>
|
||||||
<Text style={styles.time}>{value}</Text>
|
<Text style={styles.time}>{value}</Text>
|
||||||
@@ -137,15 +117,22 @@ export default ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
trackArea: {
|
||||||
|
width: '100%',
|
||||||
|
height: HANDLE_SIZE,
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
box: {
|
box: {
|
||||||
height: INITIAL_BOX_SIZE,
|
height: TRACK_HEIGHT,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
sliderHandle: {
|
sliderHandle: {
|
||||||
width: 20,
|
width: HANDLE_SIZE,
|
||||||
height: 20,
|
height: HANDLE_SIZE,
|
||||||
backgroundColor: '#f8f9ff',
|
backgroundColor: '#f8f9ff',
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -161,8 +148,15 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 3.05,
|
shadowRadius: 3.05,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
},
|
},
|
||||||
|
nativeSlider: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: HANDLE_SIZE,
|
||||||
|
zIndex: 3,
|
||||||
|
},
|
||||||
sliderTrack: {
|
sliderTrack: {
|
||||||
height: 6,
|
height: TRACK_HEIGHT,
|
||||||
backgroundColor: '#0F0C19',
|
backgroundColor: '#0F0C19',
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
Reference in New Issue
Block a user