Skip to content

Instantly share code, notes, and snippets.

@deadlinecode
Created April 15, 2024 17:15
Show Gist options
  • Select an option

  • Save deadlinecode/23551afb5f5c75778bf91adef9801ae4 to your computer and use it in GitHub Desktop.

Select an option

Save deadlinecode/23551afb5f5c75778bf91adef9801ae4 to your computer and use it in GitHub Desktop.
Expo / React Native Slider (Audio Slider with buffered bar customizable ready to use) [Web + IOS + Android]
// I used nativewind for this
// If you don't have it you need to convert the classes to the corresponding react native styles
import { memo, useEffect, useRef, useState } from 'react'
import { Animated, PanResponder, Platform, View } from 'react-native'
type ProgressSliderProps = {
// Played percentage of the track
position?: number
// Buffered percentage of the track
buffered?: number
// Color of the thumb and track
color?: {
thumb?: string
rail?: string
bufferedRail?: string
}
// Size of the round thumb
thumbSize?: number
// Size of the round thumb when touching
thumbSizeTouching?: number
// Size of the touch area around the thumb (normally bigger than the thumb itself)
thumbTouchArea?: number
// Height of the track
railHeight?: number
// Height of the touch area around the track (normally bigger than the rail itself)
railTouchArea?: number
// Fired when thumb is dropped
// Position is between a integer between 0 and 100
onSeek?: (position: number) => void
onMove?: (position: number) => void
}
export const ProgressSlider = memo(
({
position = 0,
buffered = 0,
thumbSize = 13,
thumbSizeTouching = 18,
thumbTouchArea = 30,
railHeight = 4,
railTouchArea = 16,
color,
onMove,
onSeek,
}: ProgressSliderProps) => {
const thumbPosition = useRef(new Animated.ValueXY()).current
const [trackedThumbPosition, setTrackedThumbPosition] = useState(0)
const [max, setMax] = useState(0)
const [touching, setTouching] = useState(false)
useEffect(() => {
if (!max || (!onSeek && !onMove)) return
const percentage = Math.min(Math.max(trackedThumbPosition / max, 0), 1) * 100
if (onMove) onMove(percentage)
if (onSeek && !touching) onSeek(percentage)
// This needs to be disabled since including other dependencies
// will cause the effect to emit onSeek with 0 on mount
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [trackedThumbPosition, touching])
thumbPosition.addListener(({ x }) => {
setTrackedThumbPosition(x)
})
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, { dx: thumbPosition.x }], {
useNativeDriver: false,
}),
onPanResponderRelease: () => {
thumbPosition.extractOffset()
},
})
).current
return (
<View
{...panResponder.panHandlers}
onTouchStart={(ev) => {
let x = ev.nativeEvent.locationX
if (Platform.OS === 'web') {
// Sadly web events are not typed in react native
// and react natives event properties dont overlap at all with web events
// so i had to cast it to any
const absoluteX = (ev.nativeEvent.touches[0] as any)?.clientX || 0
const rect = (ev.currentTarget as any).getBoundingClientRect()
x = absoluteX - rect.left
}
thumbPosition.setOffset({ x, y: 0 })
thumbPosition.setValue({ x: 0, y: 0 })
setTouching(true)
}}
onTouchEnd={() => {
setTouching(false)
}}
onLayout={(ev) => setMax(ev.nativeEvent.layout.width)}
className="rounded flex items-center justify-center w-full"
style={{ height: railTouchArea }}
>
<View
className="rounded w-full flex justify-center"
style={{ height: railHeight, backgroundColor: color?.rail || 'rgba(255,255,255,0.2)' }}
>
<View
className="h-full rounded absolute left-0"
style={{
width: `${buffered}%`,
backgroundColor: color?.bufferedRail || 'rgba(255,255,255,0.5)',
}}
/>
<Animated.View
className="h-full rounded absolute left-0"
style={{
width: touching
? thumbPosition.x.interpolate({
inputRange: [0, max],
outputRange: [0, max],
extrapolate: 'clamp',
})
: `${position}%`,
backgroundColor: color?.rail || 'rgba(255,255,255,1)',
}}
/>
<Animated.View
className="absolute rounded-full flex items-center justify-center cursor-pointer"
style={{
width: thumbTouchArea,
height: thumbTouchArea,
left: thumbTouchArea / -2,
transform: [
{
translateX: touching
? thumbPosition.x.interpolate({
inputRange: [0, max],
outputRange: [0, max],
extrapolate: 'clamp',
})
: max * (position / 100),
},
],
}}
{...panResponder.panHandlers}
onTouchStart={(ev) => {
ev.stopPropagation()
thumbPosition.setOffset({ x: max * (position / 100), y: 0 })
setTouching(true)
}}
onTouchEnd={() => {
setTouching(false)
}}
>
<View
className="bg-white rounded-full"
style={{
width: touching ? thumbSizeTouching : thumbSize,
height: touching ? thumbSizeTouching : thumbSize,
}}
/>
</Animated.View>
</View>
</View>
)
}
)
ProgressSlider.displayName = 'ProgressSlider'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment