Created
April 15, 2024 17:15
-
-
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]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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