UNPKG

react-native-elements

Version:
407 lines (405 loc) 16.6 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import React, { useEffect, useRef, useState } from 'react'; import { View, StyleSheet, Animated, Easing, PanResponder, Platform, } from 'react-native'; import { withTheme } from '../config'; const TRACK_SIZE = 4; const THUMB_SIZE = 40; const TRACK_STYLE = Platform.select({ web: 0, default: -1 }); const DEFAULT_ANIMATION_CONFIGS = { spring: { friction: 7, tension: 100, useNativeDriver: true, }, timing: { duration: 150, easing: Easing.inOut(Easing.ease), delay: 0, useNativeDriver: true, }, }; const getBoundedValue = (value, maximumValue, minimumValue) => { return Math.max(Math.min(value, maximumValue), minimumValue); }; class Rect { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; } containsPoint(x, y) { return (x >= this.x && y >= this.y && x <= this.x + this.width && y <= this.y + this.height); } } const Slider = (props) => { const _previousLeft = useRef(0); const [allMeasured, setAllMeasured] = useState(false); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const [trackSize, setTrackSize] = useState({ width: 0, height: 0 }); const [thumbSize, setThumbSize] = useState({ width: 0, height: 0 }); const containerSizeValue = useRef(containerSize); const trackSizeValue = useRef(trackSize); const thumbSizeValue = useRef(thumbSize); const isVertical = useRef(props.orientation === 'vertical'); const [value] = useState(new Animated.Value(getBoundedValue(props.value || 0, props.maximumValue || 1, props.minimumValue || 0))); useEffect(() => { containerSizeValue.current = containerSize; }, [containerSize]); useEffect(() => { trackSizeValue.current = trackSize; }, [trackSize]); useEffect(() => { thumbSizeValue.current = thumbSize; }, [thumbSize]); const measureContainer = (x) => { handleMeasure('containerSize', x); }; const measureTrack = (x) => { handleMeasure('trackSize', x); }; const measureThumb = (x) => { handleMeasure('thumbSize', x); }; const handleMeasure = (name, x) => { const { width: layoutWidth, height: layoutHeight } = x.nativeEvent.layout; const width = isVertical.current ? layoutHeight : layoutWidth; const height = isVertical.current ? layoutWidth : layoutHeight; const size = { width, height }; if (name === 'containerSize') { setContainerSize(size); } if (name === 'thumbSize') { setThumbSize(size); } if (name === 'trackSize') { setTrackSize(size); } if (thumbSize && trackSize && containerSize) { setAllMeasured(true); } }; const currentPropValue = useRef(props.value || 0); const prevPropValue = useRef(0); const didMountRef = useRef(false); const setCurrentValue = React.useCallback((value1) => { value.setValue(value1); }, [value]); React.useEffect(() => { setCurrentValue(props.value || 0); }, [props.value, setCurrentValue]); useEffect(() => { prevPropValue.current = props.value || 0; if (didMountRef.current) { const newValue = getBoundedValue(props.value || 0, props.maximumValue || 1, props.minimumValue || 0); if (prevPropValue.current !== newValue) { if (props.animateTransitions) { setCurrentValueAnimated(new Animated.Value(newValue)); } else { setCurrentValue(newValue); } } } else { didMountRef.current = true; } }); const setCurrentValueAnimated = (value1) => { const { animationType } = props; const animationConfig = Object.assign({}, DEFAULT_ANIMATION_CONFIGS[animationType || 'timing'], props.animationConfig, { toValue: value1, }); Animated[animationType || 'timing'](value, animationConfig).start(); }; const handleMoveShouldSetPanResponder = () => { // Should we become active when the user moves a touch over the thumb? if (!TRACK_STYLE) { return true; } return false; }; const handlePanResponderGrant = () => { _previousLeft.current = getThumbLeft(currentPropValue.current); fireChangeEvent('onSlidingStart'); }; const handlePanResponderMove = (_, gestureState) => { if (props.disabled) { return; } setCurrentValue(getValue(gestureState)); fireChangeEvent('onValueChange'); }; const handlePanResponderRequestEnd = () => { // Should we allow another component to take over this pan? return false; }; const handlePanResponderEnd = (_, gestureState) => { if (props.disabled) { return; } setCurrentValue(getValue(gestureState)); fireChangeEvent('onSlidingComplete'); }; const thumbHitTest = ({ nativeEvent }) => { const thumbTouchRect = getThumbTouchRect(); return thumbTouchRect.containsPoint(nativeEvent.locationX, nativeEvent.locationY); }; const handleStartShouldSetPanResponder = (e /* gestureState: Object */) => { // Should we become active when the user presses down on the thumb? if (!props.allowTouchTrack && !TRACK_STYLE) { return thumbHitTest(e); } if (!trackStyle) { setCurrentValue(getOnTouchValue(e)); } fireChangeEvent('onValueChange'); return true; }; const fireChangeEvent = (event) => { var _a; if (props === null || props === void 0 ? void 0 : props[event]) { (_a = props === null || props === void 0 ? void 0 : props[event]) === null || _a === void 0 ? void 0 : _a.call(props, currentPropValue.current); } }; // get value of where some touched on slider. const getOnTouchValue = ({ nativeEvent }) => { const location = isVertical.current ? nativeEvent.locationY : nativeEvent.locationX; return getValueForTouch(location); }; const getValueForTouch = (location) => { const length = containerSizeValue.current.width - thumbSizeValue.current.width; const ratio = location / length; let newValue = ratio * ((props.maximumValue || 1) - (props.minimumValue || 0)); if (props.step) { newValue = Math.round(newValue / props.step) * props.step; } return getBoundedValue(newValue + (props.minimumValue || 0), props.maximumValue || 1, props.minimumValue || 0); }; const getTouchOverflowSize = () => { const { thumbTouchSize } = props; const size = {}; if (allMeasured === true) { size.width = Math.max(0, ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE) - thumbSizeValue.current.width); size.height = Math.max(0, ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE) - containerSizeValue.current.height); } return size; }; const getTouchOverflowStyle = () => { const { width, height } = getTouchOverflowSize(); const touchOverflowStyle = {}; if (width !== undefined && height !== undefined) { const verticalMargin = -height / 2; touchOverflowStyle.marginTop = verticalMargin; touchOverflowStyle.marginBottom = verticalMargin; const horizontalMargin = -width / 2; touchOverflowStyle.marginLeft = horizontalMargin; touchOverflowStyle.marginRight = horizontalMargin; } if (props.debugTouchArea === true) { touchOverflowStyle.backgroundColor = 'orange'; touchOverflowStyle.opacity = 0.5; } return touchOverflowStyle; }; const getValue = (gestureState) => { const location = _previousLeft.current + (isVertical.current ? gestureState.dy : gestureState.dx); return getValueForTouch(location); }; React.useEffect(() => { let listenerID = value.addListener((obj) => { currentPropValue.current = obj.value; }); return () => { value.removeListener(listenerID); }; }); const getRatio = (value1) => { return ((value1 - (props.minimumValue || 0)) / ((props.maximumValue || 1) - (props.minimumValue || 0))); }; const getThumbLeft = (value1) => { const ratio = getRatio(value1); return (ratio * (containerSizeValue.current.width - thumbSizeValue.current.width)); }; const getThumbTouchRect = () => { const { thumbTouchSize } = props; const touchOverflowSize = getTouchOverflowSize(); const height = (touchOverflowSize === null || touchOverflowSize === void 0 ? void 0 : touchOverflowSize.height) / 2 + (containerSizeValue.current.height - ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE)) / 2; const width = touchOverflowSize.width / 2 + getThumbLeft(currentPropValue.current) + (thumbSizeValue.current.width - ((thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE)) / 2; if (isVertical.current) { return new Rect(height, width, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE); } return new Rect(width, height, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.width) || THUMB_SIZE, (thumbTouchSize === null || thumbTouchSize === void 0 ? void 0 : thumbTouchSize.height) || THUMB_SIZE); }; const renderDebugThumbTouchRect = (thumbLeft) => { const thumbTouchRect = getThumbTouchRect(); const positionStyle = { left: thumbLeft, top: thumbTouchRect.y, width: thumbTouchRect.width, height: thumbTouchRect.height, }; return <Animated.View style={positionStyle} pointerEvents="none"/>; }; const getMinimumTrackStyles = (thumbStart) => { const minimumTrackStyle = { position: 'absolute', }; if (isVertical.current) { minimumTrackStyle.height = Animated.add(thumbStart, thumbSizeValue.current.height / 2); minimumTrackStyle.marginLeft = trackSize.width * TRACK_STYLE; } else { minimumTrackStyle.width = Animated.add(thumbStart, thumbSizeValue.current.width / 2); minimumTrackStyle.marginTop = trackSize.height * TRACK_STYLE; } return minimumTrackStyle; }; const panResponder = useRef(PanResponder.create({ onStartShouldSetPanResponder: handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: handleMoveShouldSetPanResponder, onPanResponderGrant: handlePanResponderGrant, onPanResponderMove: handlePanResponderMove, onPanResponderRelease: handlePanResponderEnd, onPanResponderTerminationRequest: handlePanResponderRequestEnd, onPanResponderTerminate: handlePanResponderEnd, })).current; const { minimumValue, maximumValue, minimumTrackTintColor, maximumTrackTintColor, thumbTintColor, containerStyle, style, trackStyle, thumbStyle, thumbProps, debugTouchArea } = props, other = __rest(props, ["minimumValue", "maximumValue", "minimumTrackTintColor", "maximumTrackTintColor", "thumbTintColor", "containerStyle", "style", "trackStyle", "thumbStyle", "thumbProps", "debugTouchArea"]); const mainStyles = containerStyle || styles; const appliedTrackStyle = StyleSheet.flatten([styles.track, trackStyle]); const thumbStart = value.interpolate({ inputRange: [minimumValue || 0, maximumValue || 1], outputRange: [0, containerSize.width - thumbSize.width], }); const valueVisibleStyle = {}; if (!allMeasured) { valueVisibleStyle.height = 0; valueVisibleStyle.width = 0; } const minimumTrackStyle = Object.assign(Object.assign(Object.assign({}, getMinimumTrackStyles(thumbStart)), { backgroundColor: minimumTrackTintColor }), valueVisibleStyle); const touchOverflowStyle = getTouchOverflowStyle(); return (<View {...other} style={StyleSheet.flatten([ isVertical.current ? mainStyles.containerVertical : mainStyles.containerHorizontal, style, ])} onLayout={measureContainer} accessibilityRole="adjustable" accessibilityValue={{ min: minimumValue, max: maximumValue, now: props.value, }}> <View style={StyleSheet.flatten([ mainStyles.track, isVertical.current ? mainStyles.trackVertical : mainStyles.trackHorizontal, appliedTrackStyle, { backgroundColor: maximumTrackTintColor }, ])} onLayout={measureTrack}/> <Animated.View style={StyleSheet.flatten([ mainStyles.track, isVertical.current ? mainStyles.trackVertical : mainStyles.trackHorizontal, appliedTrackStyle, minimumTrackStyle, ])}/> <SliderThumb isVisible={allMeasured} onLayout={measureThumb} style={thumbStyle} color={thumbTintColor} start={thumbStart} vertical={isVertical.current} {...thumbProps}/> <View style={StyleSheet.flatten([styles.touchArea, touchOverflowStyle])} {...panResponder.panHandlers}> {debugTouchArea === true && renderDebugThumbTouchRect(thumbStart)} </View> </View>); }; const SliderThumb = (_a) => { var { Component, isVisible, onLayout, style, start, color, vertical } = _a, props = __rest(_a, ["Component", "isVisible", "onLayout", "style", "start", "color", "vertical"]); const ThumbComponent = Component || Animated.View; const axis = vertical ? 'translateY' : 'translateX'; const thumbPosition = [{ [axis]: start }]; const styleTransform = (style && style.transform) || []; const visibleStyle = isVisible ? {} : { height: 0, width: 0 }; return (<ThumbComponent testID="sliderThumb" onLayout={onLayout} style={StyleSheet.flatten([ Object.assign({ backgroundColor: color, transform: [...thumbPosition, ...styleTransform] }, visibleStyle), styles.thumb, style, ])} {...props}/>); }; Slider.defaultProps = { value: 0, minimumValue: 0, maximumValue: 1, step: 0, minimumTrackTintColor: '#3f3f3f', maximumTrackTintColor: '#b3b3b3', allowTouchTrack: false, thumbTintColor: 'red', thumbTouchSize: { width: THUMB_SIZE, height: THUMB_SIZE }, debugTouchArea: false, animationType: 'timing', orientation: 'horizontal', }; const styles = StyleSheet.create({ containerHorizontal: { height: 40, justifyContent: 'center', }, containerVertical: { width: 40, flexDirection: 'column', alignItems: 'center', }, track: { borderRadius: TRACK_SIZE / 2, }, trackHorizontal: { height: TRACK_SIZE, }, trackVertical: { flex: 1, width: TRACK_SIZE, }, thumb: { position: 'absolute', width: THUMB_SIZE, height: THUMB_SIZE, borderRadius: THUMB_SIZE / 2, }, touchArea: { position: 'absolute', backgroundColor: 'transparent', top: 0, left: 0, right: 0, bottom: 0, }, debugThumbTouchArea: { position: 'absolute', backgroundColor: 'green', opacity: 0.5, }, }); export { Slider }; export default withTheme(Slider, 'Slider');