UNPKG

react-native-reanimated-slider

Version:
403 lines (389 loc) 10.7 kB
import * as React from "react"; import { I18nManager, View } from "react-native"; import Animated from "react-native-reanimated"; import { PanGestureHandler, State } from "react-native-gesture-handler"; import Ballon from "./Ballon"; const { Value, event, cond, eq, set, clockRunning, startClock, spring, stopClock, Extrapolate, sub, Clock, divide, call, interpolateNode, multiply, block, or } = Animated; const BUBBLE_WIDTH = 100; type Props = { /** * color to fill the progress in the seekbar */ minimumTrackTintColor?: string, /** * color to fill the background in the seekbar */ maximumTrackTintColor?: string, /** * color to fill the cache in the seekbar */ cacheTrackTintColor?: string, /** * style for the container view */ style?: any, /** * color of the border of the slider */ borderColor?: string, /** * a function that gets the current value of the slider as you slide it, * and returns a string to be used inside the ballon. if not provided it will use the * current value as integer. */ ballon?: number => string, /** * an AnimatedValue from `react-native-reanimated` library which is the * current value of the slider. */ progress: typeof Animated.Value, /** * an AnimatedValue from `react-native-reanimated` library which is the * curren value of the cache. the cache is optional and will be rendered behind * the main progress indicator. */ cache?: typeof Animated.Value, /** * an AnimatedValue from `react-native-reanimated` library which is the * minimum value of the slider. */ min: typeof Animated.Value, /** * an AnimatedValue from `react-native-reanimated` library which is the * maximum value of the slider. */ max: typeof Animated.Value, /** * callback called when the users starts sliding */ onSlidingStart: () => void, /** * callback called when the users stops sliding. the new value will be passed as * argument */ onSlidingComplete: number => void, /** * render custom Ballon to show when sliding. */ renderBallon?: () => React.ReactNode, /** * this function will be called while sliding, and should set the text inside your custom * ballon. */ setBallonText?: string => void, /** * value to pass to the container of the ballon as `translateY` */ ballonTranslateY: number, /** * render custom thumb image. */ renderThumbImage?: () => React.ReactNode, /** * thumb offset from the end of seek */ thumbOffset?: number }; /** * The slider component to show progress. you should install and link `react-native-reanimated` * and `react-native-gesture-handler` to be able to use it. All animated values must be imported from * `react-native-reanimated`. * * ## Usage * * ```js * import Slider from 'react-native-reanimated-slider'; * import { Value } from 'react-native-reanimated'; * ... * * const textRef = useRef() * * renderBallon=()=>( * <View> * <TextInput ref={textRef} /> * </View> * ) * * setBallonText=(text)=>{ * textRef.setNativeProps({text}) * } * * currentTime= new Value(10) * playableDuration= new Value(15) * seekableDuration= new Value(20) * * const slidingStart = ()=>{ * console.log('slide started') * } * const slidingComplete = (number)=>{ * console.log('slide completed' + number) * } * ... * return ( * <Slider * style={{ flex: 1 }} * minimumTrackTintColor="#fff" * thumbTintColor="#f00" * borderColor="#0f0" * progress={currentTime} * min={new Reanimated.Value(0)} * cache={playableDuration} * max={seekableDuration} * onSlidingStart={slidingStart} * onSlidingComplete={slidingComplete} * * // only if you want to render custom ballon for sliding * // renderBallon={this.renderBallon} * // setBallonText={this.setBallonText} * /> * ) * ``` * * */ class Slider extends React.Component<Props> { static defaultProps = { minimumTrackTintColor: "#f3f", maximumTrackTintColor: "transparent", cacheTrackTintColor: "#777", borderColor: "#fff", thumbOffset: 7, ballonTranslateY: -25, }; ballon = React.createRef(); constructor(props) { const { progress, min, max, cache } = props; super(props); this.convert_to_percent = value => cond(eq(min, max), 0, divide(value, sub(max, min))); this.gestureState = new Value(0); this.x = new Value(0); this.width = new Value(0); this.progress_x = multiply(this.convert_to_percent(progress), this.width); this.cache_x = multiply(this.convert_to_percent(cache), this.width); this.clamped_x = cond( eq(this.width, 0), 0, interpolateNode(this.x, { inputRange: [0, this.width], outputRange: [0, this.width], extrapolate: Extrapolate.CLAMP }) ); this.value_x = divide(multiply(this.clamped_x, max), this.width); this.onGestureEvent = event([ { nativeEvent: { state: this.gestureState, x: this.x } } ]); this.clock = new Clock(); this.seek = block([ cond( or( eq(this.gestureState, State.ACTIVE), eq(this.gestureState, State.BEGAN) ), [ call([this.value_x], x => { this.props.setBallonText ? this.props.setBallonText(props.ballon? props.ballon(x[0]) : x[0].toFixed()) : this.ballon.current.setText(props.ballon? props.ballon(x[0]) : x[0].toFixed()); }), cond( eq(this.gestureState, State.BEGAN), call([this.value_x], () => props.onSlidingStart && props.onSlidingStart()) ), this.clamped_x ], [ cond( eq(this.gestureState, State.END), [ set(this.gestureState, State.UNDETERMINED), call([this.value_x], x => props.onSlidingComplete && props.onSlidingComplete(x[0])), this.clamped_x ], [this.progress_x] ) ] ) ]); this.thumb = sub(this.seek, this.props.thumbOffset); this.spring_state = { finished: new Value(0), velocity: new Value(0), position: new Value(0), time: new Value(0) }; this.runspring = ({ toValue }) => { const config = { damping: 10, mass: 1, stiffness: 150, overshootClamping: false, restSpeedThreshold: 0.001, restDisplacementThreshold: 0.001, toValue: new Value(0) }; return [ cond(clockRunning(this.clock), 0, [ set(this.spring_state.finished, 0), set(this.spring_state.velocity, 0), // set(this.spring_state.position, from), set(config.toValue, toValue), startClock(this.clock) ]), spring(this.clock, this.spring_state, config), cond(this.spring_state.finished, [stopClock(this.clock)]), this.spring_state.position ]; }; this.height = cond( or( eq(this.gestureState, State.BEGAN), eq(this.gestureState, State.ACTIVE) ), [this.runspring({ from: 0, toValue: 1 })], cond( or( eq(this.gestureState, State.UNDETERMINED), eq(this.gestureState, State.END) ), [this.runspring({ from: 1, toValue: 0 })], this.spring_state.position ) ); this.state = { ballon: "" }; } _onLayout = ({ nativeEvent }) => { this.width.setValue(nativeEvent.layout.width); }; _renderBallon = () => { return <Ballon ref={this.ballon} />; }; _renderThumbImage = style => { return <View style={style} />; }; render() { const { ballon } = this.state; const { renderBallon, renderThumbImage, style, minimumTrackTintColor, maximumTrackTintColor, cacheTrackTintColor, borderColor, ballonTranslateY, thumbOffset } = this.props; const ballonRenderer = renderBallon || this._renderBallon; const thumbRenderer = renderThumbImage || this._renderThumbImage; return ( <PanGestureHandler onGestureEvent={this.onGestureEvent} onHandlerStateChange={this.onGestureEvent} minDist={0}> <Animated.View style={[ { flex: 1, height: 30, overflow: "visible", alignItems: "center", justifyContent: "center", backgroundColor: "#3330" }, style ]} onLayout={this._onLayout}> <Animated.View style={{ width: "100%", height: 5, borderRadius: 2, borderColor: borderColor, overflow: "hidden", borderWidth: 1, backgroundColor: maximumTrackTintColor }}> <Animated.View style={{ backgroundColor: cacheTrackTintColor, height: "100%", width: this.cache_x, [I18nManager.isRTL ? "right" : "left"]: 0, position: "absolute" }} /> <Animated.View style={{ backgroundColor: minimumTrackTintColor, height: "100%", maxWidth: "100%", width: this.seek, [I18nManager.isRTL ? "right" : "left"]: 0, position: "absolute" }} /> </Animated.View> <Animated.View style={{ [I18nManager.isRTL ? "right" : "left"]: this.thumb, position: "absolute" }}> {thumbRenderer({ backgroundColor: minimumTrackTintColor, height: 15, width: 15, borderRadius: 30 })} </Animated.View> <Animated.View style={{ position: "absolute", [I18nManager.isRTL ? "right" : "left"]: -50, width: BUBBLE_WIDTH, opacity: this.height, transform: [ { translateY: ballonTranslateY }, { translateX: this.clamped_x }, { scale: this.height } ] }}> {ballonRenderer({ text: ballon })} </Animated.View> </Animated.View> </PanGestureHandler> ); } } export default Slider;