UNPKG

@fto-consult/expo-ui

Version:

Bibliothèque de composants UI Expo,react-native

770 lines (688 loc) • 25.2 kB
/*** usage, @see : https://github.com/miblanchard/react-native-slider */ import React, {PureComponent} from 'react'; import { Animated, Easing, I18nManager, Image, PanResponder, } from 'react-native'; // styles import View from "$ecomponents/View"; import {defaultStyles as styles} from './styles'; const Rect = ({ height, width, x, y, }) => ({ containsPoint: (nativeX, nativeY) => nativeX >= x && nativeY >= y && nativeX <= x + width && nativeY <= y + height, height, trackDistanceToPoint: (nativeX) => { if (nativeX < x) { return x - nativeX; } if (nativeX > x + width) { return nativeX - (x + width); } return 0; }, width, x, y, }); const DEFAULT_ANIMATION_CONFIGS = { spring: { friction: 7, tension: 100, }, timing: { duration: 150, easing: Easing.inOut(Easing.ease), delay: 0, }, }; const normalizeValue = (props,value) => { if (!value || (Array.isArray(value) && value.length === 0)) { return [0]; } const {maximumValue, minimumValue} = props; const getBetweenValue = (inputValue) => Math.max(Math.min(inputValue, maximumValue), minimumValue); if (!Array.isArray(value)) { return [getBetweenValue(value)]; } return value.map(getBetweenValue).sort((a, b) => a - b); }; const updateValues = ({ values, newValues = values, }) => { if ( Array.isArray(newValues) && Array.isArray(values) && newValues.length !== values.length ) { return updateValues({values: newValues}); } if (Array.isArray(values) && Array.isArray(newValues)) { return values?.map((value, index) => { let valueToSet = newValues[index]; if (value instanceof Animated.Value) { if (valueToSet instanceof Animated.Value) { valueToSet = valueToSet.__getValue(); } value.setValue(valueToSet); return value; } if (valueToSet instanceof Animated.Value) { return valueToSet; } return new Animated.Value(valueToSet); }); } return [new Animated.Value(0)]; }; const indexOfLowest = (values) => { let lowestIndex = 0; values.forEach((value, index, array) => { if (value < array[lowestIndex]) { lowestIndex = index; } }); return lowestIndex; }; export default class Slider extends PureComponent { constructor(props) { super(props); this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder, onMoveShouldSetPanResponder: this._handleMoveShouldSetPanResponder, onPanResponderGrant: this._handlePanResponderGrant, onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminationRequest: this._handlePanResponderRequestEnd, onPanResponderTerminate: this._handlePanResponderEnd, }); this.state = { allMeasured: false, containerSize: { width: 0, height: 0, }, thumbSize: { width: 0, height: 0, }, trackMarksValues: updateValues({ values: normalizeValue(this.props, this.props.trackMarks), }), values: updateValues({ values: normalizeValue( this.props, this.props.value instanceof Animated.Value ? this.props.value.__getValue() : this.props.value, ), }), }; } static defaultProps = { animationType: 'timing', debugTouchArea: false, trackMarks: [], maximumTrackTintColor: '#b3b3b3', maximumValue: 1, minimumTrackTintColor: '#3f3f3f', minimumValue: 0, step: 0, thumbTintColor: '#343434', trackClickable: true, value: 0, vertical: false, }; static getDerivedStateFromProps(props, state) { if ( props.trackMarks && !!state.trackMarksValues && state.trackMarksValues.length > 0 ) { const newTrackMarkValues = normalizeValue(props, props.trackMarks); const statePatch = {}; if ( state.trackMarksValues && newTrackMarkValues.length !== state.trackMarksValues.length ) { statePatch.trackMarksValues = updateValues({ values: state.trackMarksValues, newValues: newTrackMarkValues, }); } return statePatch; } } componentDidUpdate() { const newValues = normalizeValue( this.props, this.props.value instanceof Animated.Value ? this.props.value.__getValue() : this.props.value, ); newValues.forEach((value, i) => { if (!this.state.values[i]) { this._setCurrentValue(value, i); } else if (value !== this.state.values[i].__getValue()) { if (this.props.animateTransitions) { this._setCurrentValueAnimated(value, i); } else { this._setCurrentValue(value, i); } } }); } _getRawValues(values) { return values.map((value) => value.__getValue()); } _handleStartShouldSetPanResponder = (e) => this._thumbHitTest(e); // Should we become active when the user presses down on the thumb? _handleMoveShouldSetPanResponder() { // Should we become active when the user moves a touch over the thumb? return false; } _handlePanResponderGrant = (e) => { const {thumbSize} = this.state; const {nativeEvent} = e; this._previousLeft = this.props.trackClickable ? nativeEvent.locationX - thumbSize.width : this._getThumbLeft(this._getCurrentValue(this._activeThumbIndex)); this.props?.onSlidingStart?.(this._getRawValues(this.state.values)); }; _handlePanResponderMove = (_e, gestureState) => { if (this.props.disabled) { return; } this._setCurrentValue( this._getValue(gestureState), this._activeThumbIndex, () => { this.props?.onValueChange?.( this._getRawValues(this.state.values), ); }, ); }; _handlePanResponderRequestEnd = () => /* e, gestureState: GestureState */ { // Should we allow another component to take over this pan? return false; }; _handlePanResponderEnd = (_e, gestureState) => { if (this.props.disabled) { return; } this._setCurrentValue( this._getValue(gestureState), this._activeThumbIndex, () => { if (this.props.trackClickable) { this.props?.onValueChange?.( this._getRawValues(this.state.values), ); } this.props?.onSlidingComplete?.( this._getRawValues(this.state.values), ); }, ); this._activeThumbIndex = 0; }; _measureContainer = (e) => { this._handleMeasure('_containerSize', e); }; _measureTrack = (e) => { this._handleMeasure('_trackSize', e); }; _measureThumb = (e) => { this._handleMeasure('_thumbSize', e); }; _handleMeasure = ( name, e, ) => { const {width, height} = e.nativeEvent.layout; const size = { width, height, }; const currentSize = this[name]; if ( currentSize && width === currentSize.width && height === currentSize.height ) { return; } this[name] = size; if (this._containerSize && this._thumbSize) { this.setState({ containerSize: this._containerSize, thumbSize: this._thumbSize, allMeasured: true, }); } }; _getRatio = (value) => { const {maximumValue, minimumValue} = this.props; return (value - minimumValue) / (maximumValue - minimumValue); }; _getThumbLeft = (value) => { const {containerSize, thumbSize} = this.state; const {vertical} = this.props; const standardRatio = this._getRatio(value); const ratio = I18nManager.isRTL ? 1 - standardRatio : standardRatio; return ratio * ((vertical ? containerSize.height : containerSize.width) - thumbSize.width); }; _getValue = (gestureState) => { const {containerSize, thumbSize, values} = this.state; const {maximumValue, minimumValue, step, vertical} = this.props; const length = containerSize.width - thumbSize.width; const thumbLeft = vertical ? this._previousLeft + (gestureState.dy * -1) : this._previousLeft + gestureState.dx; const nonRtlRatio = thumbLeft / length; const ratio = I18nManager.isRTL ? 1 - nonRtlRatio : nonRtlRatio; let minValue = minimumValue; let maxValue = maximumValue; const rawValues = this._getRawValues(values); const buffer = step ? step : 0.1; if (values.length === 2) { if (this._activeThumbIndex === 1) { minValue = rawValues[0] + buffer; } else { maxValue = rawValues[1] - buffer; } } if (step) { return Math.max( minValue, Math.min( maxValue, minimumValue + Math.round( (ratio * (maximumValue - minimumValue)) / step, ) * step, ), ); } return Math.max( minValue, Math.min( maxValue, ratio * (maximumValue - minimumValue) + minimumValue, ), ); }; _getCurrentValue = (thumbIndex = 0) => this.state.values[thumbIndex].__getValue(); _setCurrentValue = ( value, thumbIndex, callback, ) => { const safeIndex = thumbIndex ?? 0; const animatedValue = this.state.values[safeIndex]; if (animatedValue) { animatedValue.setValue(value); if (callback) { callback(); } } else { this.setState((prevState) => { const newValues = [...prevState.values]; newValues[safeIndex] = new Animated.Value(value); return { values: newValues, }; }, callback); } }; _setCurrentValueAnimated = (value, thumbIndex = 0) => { const {animationType} = this.props; const animationConfig = { ...DEFAULT_ANIMATION_CONFIGS[animationType], ...this.props.animationConfig, toValue: value, useNativeDriver: false, }; Animated[animationType]( this.state.values[thumbIndex], animationConfig, ).start(); }; _getTouchOverflowSize = () => { const {allMeasured, containerSize, thumbSize} = this.state; const {thumbTouchSize} = this.props; const size = { width: 40, height: 40, }; if (allMeasured) { size.width = Math.max( 0, thumbTouchSize?.width || 0 - thumbSize.width, ); size.height = Math.max( 0, thumbTouchSize?.height || 0 - containerSize.height, ); } return size; }; _getTouchOverflowStyle = () => { const {width, height} = this._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 (this.props.debugTouchArea === true) { touchOverflowStyle.backgroundColor = 'orange'; touchOverflowStyle.opacity = 0.5; } return touchOverflowStyle; }; _thumbHitTest = (e) => { const {nativeEvent} = e; const {trackClickable} = this.props; const {values} = this.state; const hitThumb = values.find((_, i) => { const thumbTouchRect = this._getThumbTouchRect(i); const containsPoint = thumbTouchRect.containsPoint( nativeEvent.locationX, nativeEvent.locationY, ); if (containsPoint) { this._activeThumbIndex = i; } return containsPoint; }); if (hitThumb) { return true; } if (trackClickable) { // set the active thumb index if (values.length === 1) { this._activeThumbIndex = 0; } else { // we will find the closest thumb and that will be the active thumb const thumbDistances = values.map((_value, index) => { const thumbTouchRect = this._getThumbTouchRect(index); return thumbTouchRect.trackDistanceToPoint( nativeEvent.locationX, ); }); this._activeThumbIndex = indexOfLowest(thumbDistances); } return true; } return false; }; _getThumbTouchRect = (thumbIndex = 0) => { const {containerSize, thumbSize} = this.state; const {thumbTouchSize} = this.props; const {height, width} = thumbTouchSize || {height: 40, width: 40}; const touchOverflowSize = this._getTouchOverflowSize(); return Rect({ height, width, x: touchOverflowSize.width / 2 + this._getThumbLeft(this._getCurrentValue(thumbIndex)) + (thumbSize.width - width) / 2, y: touchOverflowSize.height / 2 + (containerSize.height - height) / 2, }); }; _activeThumbIndex = 0; _containerSize; _panResponder; _previousLeft = 0; _thumbSize; _trackSize; _renderDebugThumbTouchRect = ( thumbLeft, index, ) => { const {height, y, width} = this._getThumbTouchRect() || {}; const positionStyle = { height, left: thumbLeft, top: y, width, }; return ( <Animated.View key={`debug-thumb-${index}`} pointerEvents="none" style={[styles.debugThumbTouchArea, positionStyle]} /> ); }; _renderThumbImage = (thumbIndex = 0) => { const {thumbImage} = this.props; if (!thumbImage) { return null; } return ( <Image source={ (Array.isArray(thumbImage) ? thumbImage[thumbIndex] : thumbImage) } /> ); }; render() { const { containerStyle, debugTouchArea, maximumTrackTintColor, maximumValue, minimumTrackTintColor, minimumValue, renderAboveThumbComponent, renderTrackMarkComponent, renderThumbComponent, thumbStyle, thumbTintColor, trackStyle, vertical, ...other } = this.props; const { allMeasured, containerSize, thumbSize, trackMarksValues, values, } = this.state; const interpolatedThumbValues = values.map((value) => value.interpolate({ inputRange: [minimumValue, maximumValue], outputRange: I18nManager.isRTL ? [0, -(containerSize.width - thumbSize.width)] : [0, containerSize.width - thumbSize.width], }), ); const interpolatedTrackValues = values.map((value) => value.interpolate({ inputRange: [minimumValue, maximumValue], outputRange: [0, containerSize.width - thumbSize.width], }), ); const interpolatedTrackMarksValues = trackMarksValues && trackMarksValues.map((v) => v.interpolate({ inputRange: [minimumValue, maximumValue], outputRange: I18nManager.isRTL ? [0, -(containerSize.width - thumbSize.width)] : [0, containerSize.width - thumbSize.width], }), ); const valueVisibleStyle = {}; if (!allMeasured) { valueVisibleStyle.opacity = 0; } const interpolatedRawValues = this._getRawValues( interpolatedTrackValues, ); const minThumbValue = new Animated.Value( Math.min(...interpolatedRawValues), ); const maxThumbValue = new Animated.Value( Math.max(...interpolatedRawValues), ); const minimumTrackStyle = { position: 'absolute', left: interpolatedTrackValues.length === 1 ? new Animated.Value(0) : Animated.add(minThumbValue, thumbSize.width / 2), width: interpolatedTrackValues.length === 1 ? Animated.add( interpolatedTrackValues[0], thumbSize.width / 2, ) : Animated.add( Animated.multiply(minThumbValue, -1), maxThumbValue, ), backgroundColor: minimumTrackTintColor, ...valueVisibleStyle, }; const touchOverflowStyle = this._getTouchOverflowStyle(); return ( <> {renderAboveThumbComponent && ( <View style={styles.aboveThumbComponentsContainer}> {interpolatedThumbValues.map((value, i) => ( <Animated.View key={`slider-above-thumb-${i}`} style={[ styles.renderThumbComponent, // eslint-disable-next-line react-native/no-inline-styles { bottom: 0, transform: [ { translateX: value, }, { translateY: 0, }, ], ...valueVisibleStyle, }, ]}> {renderAboveThumbComponent(i)} </Animated.View> ))} </View> )} <View {...other} style={[styles.container, vertical ? {transform: [{rotate: '-90deg' }]} : {}, containerStyle]} onLayout={this._measureContainer}> <View renderToHardwareTextureAndroid style={[ styles.track, { backgroundColor: maximumTrackTintColor, }, trackStyle, ]} onLayout={this._measureTrack} /> <Animated.View renderToHardwareTextureAndroid style={[styles.track, trackStyle, minimumTrackStyle]} /> {renderTrackMarkComponent && interpolatedTrackMarksValues && interpolatedTrackMarksValues.map((value, i) => ( <Animated.View key={`track-mark-${i}`} style={[ styles.renderThumbComponent, { transform: [ { translateX: value, }, { translateY: 0, }, ], ...valueVisibleStyle, }, ]}> {renderTrackMarkComponent(i)} </Animated.View> ))} {interpolatedThumbValues.map((value, i) => ( <Animated.View key={`slider-thumb-${i}`} style={[ renderThumbComponent ? styles.renderThumbComponent : styles.thumb, renderThumbComponent ? {} : { backgroundColor: thumbTintColor, ...thumbStyle, }, { transform: [ { translateX: value, }, { translateY: 0, }, ], ...valueVisibleStyle, }, ]} onLayout={this._measureThumb}> {renderThumbComponent ? renderThumbComponent() : this._renderThumbImage(i)} </Animated.View> ))} <View style={[styles.touchArea, touchOverflowStyle]} {...this._panResponder.panHandlers}> {!!debugTouchArea && interpolatedThumbValues.map((value, i) => this._renderDebugThumbTouchRect(value, i), )} </View> </View> </> ); } }