UNPKG

react-native-ui-lib

Version:

<p align="center"> <img src="https://user-images.githubusercontent.com/1780255/105469025-56759000-5ca0-11eb-993d-3568c1fd54f4.png" height="250px" style="display:block"/> </p> <p align="center">UI Toolset & Components Library for React Native</p> <p a

637 lines (565 loc) • 15.9 kB
import _pt from "prop-types"; import _ from 'lodash'; import React, { PureComponent } from 'react'; import { StyleSheet, PanResponder, AccessibilityInfo, Animated } from 'react-native'; import { Constants } from "../../commons/new"; import { Colors } from "../../style"; import View from "../view"; import { extractAccessibilityProps } from "../../commons/modifiers"; const TRACK_SIZE = 6; const THUMB_SIZE = 24; const BORDER_WIDTH = 6; const SHADOW_RADIUS = 4; const DEFAULT_COLOR = Colors.grey50; const ACTIVE_COLOR = Colors.violet30; const INACTIVE_COLOR = Colors.grey60; const defaultProps = { value: 0, minimumValue: 0, maximumValue: 1, step: 0, thumbHitSlop: { top: 10, bottom: 10, left: 24, right: 24 } }; /** * @description: A Slider component * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/SliderScreen.tsx * @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Slider/Slider.gif?raw=true */ export default class Slider extends PureComponent { static propTypes = { /** * Initial value */ value: _pt.number, /** * Minimum value */ minimumValue: _pt.number, /** * Maximum value */ maximumValue: _pt.number, /** * Step value of the slider. The value should be between 0 and (maximumValue - minimumValue) */ step: _pt.number, /** * The color used for the track from minimum value to current value */ minimumTrackTintColor: _pt.string, /** * The track color */ maximumTrackTintColor: _pt.string, /** * Custom render instead of rendering the track */ renderTrack: _pt.func, /** * Thumb color */ thumbTintColor: _pt.string, /** * Callback for onValueChange */ onValueChange: _pt.func, /** * Callback that notifies about slider seeking is started */ onSeekStart: _pt.func, /** * Callback that notifies about slider seeking is finished */ onSeekEnd: _pt.func, /** * If true the Slider will not change it's style on press */ disableActiveStyling: _pt.bool, /** * If true the Slider will be disabled and will appear in disabled color */ disabled: _pt.bool, /** * If true the Slider will stay in LTR mode even if the app is on RTL mode */ disableRTL: _pt.bool, /** * If true the component will have accessibility features enabled */ accessible: _pt.bool, /** * The slider's test identifier */ testID: _pt.string }; static displayName = 'Slider'; static defaultProps = defaultProps; thumb = undefined; _thumbStyles = {}; minTrack = undefined; _minTrackStyles = {}; _x = 0; _dx = 0; _thumbAnimationConstants = { duration: 100, defaultScaleFactor: 1.5 }; initialValue = this.getRoundedValue(this.props.value); lastValue = this.initialValue; initialThumbSize = { width: THUMB_SIZE, height: THUMB_SIZE }; constructor(props) { super(props); this.state = { containerSize: { width: 0, height: 0 }, trackSize: { width: 0, height: 0 }, thumbSize: { width: 0, height: 0 }, thumbActiveAnimation: new Animated.Value(1), measureCompleted: false }; this.checkProps(props); this._panResponder = PanResponder.create({ onMoveShouldSetPanResponder: this.handleMoveShouldSetPanResponder, onPanResponderGrant: this.handlePanResponderGrant, onPanResponderMove: this.handlePanResponderMove, onPanResponderRelease: this.handlePanResponderEnd, onStartShouldSetPanResponder: () => true, onPanResponderEnd: () => true, onPanResponderTerminationRequest: () => false }); } checkProps(props) { if (props.minimumValue >= props.maximumValue) { console.warn('Slider minimumValue must be lower than maximumValue'); } if (props.value < props.minimumValue || props.value > props.maximumValue) { console.warn('Slider value is not in range'); } } getAccessibilityProps() { const { disabled } = this.props; return { accessibilityLabel: 'Slider', accessible: true, accessibilityRole: 'adjustable', accessibilityStates: disabled ? ['disabled'] : [], accessibilityActions: [{ name: 'increment', label: 'increment' }, { name: 'decrement', label: 'decrement' }], ...extractAccessibilityProps(this.props) }; } componentDidUpdate(prevProps, prevState) { if (prevProps.value !== this.props.value) { this.initialValue = this.getRoundedValue(this.props.value); // set position for new value this._x = this.getXForValue(this.initialValue); this.updateStyles(this._x); } if (prevState.measureCompleted !== this.state.measureCompleted) { this.initialThumbSize = this.state.thumbSize; // for thumb enlargement // set initial position this._x = this.getXForValue(this.initialValue); this.updateStyles(this._x); } } componentDidMount() { this.dimensionsChangeListener = Constants.addDimensionsEventListener(this.onOrientationChanged); } componentWillUnmount() { Constants.removeDimensionsEventListener(this.dimensionsChangeListener || this.onOrientationChanged); } /* Gesture Recognizer */ handleMoveShouldSetPanResponder = () => { return true; }; handlePanResponderGrant = () => { this.updateThumbStyle(true); this._dx = 0; this.onSeekStart(); }; handlePanResponderMove = (_e, gestureState) => { const { disabled, disableRTL } = this.props; if (disabled) { return; } const dx = gestureState.dx * (Constants.isRTL && !disableRTL ? -1 : 1); this.update(dx - this._dx); this._dx = dx; }; handlePanResponderEnd = () => { this.updateThumbStyle(false); this.bounceToStep(); this.onSeekEnd(); }; /* Actions */ update(dx) { // calc x in range (instead of: this._x += dx) let x = this._x; x += dx; x = Math.max(Math.min(x, this.state.trackSize.width), 0); this._x = x; this.updateStyles(this._x); this.updateValue(this._x); } bounceToStep() { if (this.props.step > 0) { const v = this.getValueForX(this._x); const round = this.getRoundedValue(v); const x = this.getXForValue(round); this._x = x; this.updateStyles(x); } } updateStyles(x) { if (this.thumb) { const { disableRTL } = this.props; const { trackSize } = this.state; const _x = Constants.isRTL && disableRTL ? trackSize.width - x : x; const position = _x - this.initialThumbSize.width / 2; const deviation = 3; if (position + deviation < 0) { this._thumbStyles.left = 0; } else if (position - deviation > trackSize.width - this.initialThumbSize.width) { this._thumbStyles.left = trackSize.width - this.initialThumbSize.width; } else { this._thumbStyles.left = position; } this.thumb.setNativeProps(this._thumbStyles); } if (this.minTrack) { this._minTrackStyles.width = Math.min(this.state.trackSize.width, x); this.minTrack.setNativeProps(this._minTrackStyles); } } updateValue(x) { const value = this.getValueForX(x); this.onValueChange(value); } updateThumbStyle(start) { if (this.thumb && !this.props.disableActiveStyling) { const { thumbStyle, activeThumbStyle } = this.props; const style = thumbStyle || styles.thumb; const activeStyle = activeThumbStyle || styles.activeThumb; const activeOrInactiveStyle = !this.props.disabled ? start ? activeStyle : style : {}; this._thumbStyles.style = _.omit(activeOrInactiveStyle, 'height', 'width'); this.thumb.setNativeProps(this._thumbStyles); this.scaleThumb(start); } } scaleThumb = start => { const scaleFactor = start ? this.calculatedThumbActiveScale() : 1; this.thumbAnimationAction(scaleFactor); }; thumbAnimationAction = toValue => { const { thumbActiveAnimation } = this.state; const { duration } = this._thumbAnimationConstants; Animated.timing(thumbActiveAnimation, { toValue, duration, useNativeDriver: true }).start(); }; getRoundedValue(value) { const { step } = this.props; const v = this.getValueInRange(value); return step > 0 ? Math.round(v / step) * step : v; } getValueInRange(value) { const { minimumValue, maximumValue } = this.props; const v = value < minimumValue ? minimumValue : value > maximumValue ? maximumValue : value; return v; } getXForValue(v) { const { minimumValue } = this.props; const range = this.getRange(); const relativeValue = minimumValue - v; const value = minimumValue < 0 ? Math.abs(relativeValue) : v - minimumValue; // for negatives const ratio = value / range; const x = ratio * this.state.trackSize.width; return x; } getValueForX(x) { const { maximumValue, minimumValue, step } = this.props; const ratio = x / (this.state.trackSize.width - this.initialThumbSize.width / 2); const range = this.getRange(); if (step) { return Math.max(minimumValue, Math.min(maximumValue, minimumValue + Math.round(ratio * range / step) * step)); } else { return Math.max(minimumValue, Math.min(maximumValue, ratio * range + minimumValue)); } } getRange() { const { minimumValue, maximumValue } = this.props; const range = maximumValue - minimumValue; return range; } setMinTrackRef = ref => { this.minTrack = ref; }; setThumbRef = ref => { this.thumb = ref; }; calculatedThumbActiveScale = () => { const { activeThumbStyle, thumbStyle, disabled, disableActiveStyling } = this.props; if (disabled || disableActiveStyling) { return 1; } const { defaultScaleFactor } = this._thumbAnimationConstants; if (!activeThumbStyle || !thumbStyle) { return defaultScaleFactor; } const scaleRatioFromSize = Number(activeThumbStyle.height) / Number(thumbStyle.height); return scaleRatioFromSize || defaultScaleFactor; }; updateTrackStepAndStyle = ({ nativeEvent }) => { const { disableRTL, step } = this.props; const { trackSize } = this.state; this._x = Constants.isRTL && !disableRTL ? trackSize.width - nativeEvent.locationX : nativeEvent.locationX; this.updateValue(this._x); if (step > 0) { this.bounceToStep(); } else { this.updateStyles(this._x); } }; onOrientationChanged = () => { this.initialValue = this.lastValue; this.setState({ measureCompleted: false }); }; /* Events */ onValueChange = value => { this.lastValue = value; _.invoke(this.props, 'onValueChange', value); }; onSeekStart() { _.invoke(this.props, 'onSeekStart'); } onSeekEnd() { _.invoke(this.props, 'onSeekEnd'); } onContainerLayout = nativeEvent => { this.handleMeasure('containerSize', nativeEvent); }; onTrackLayout = nativeEvent => { this.setState({ measureCompleted: false }); this.handleMeasure('trackSize', nativeEvent); }; onThumbLayout = nativeEvent => { this.handleMeasure('thumbSize', nativeEvent); }; handleTrackPress = event => { if (this.props.disabled) { return; } this.onSeekStart(); this.updateTrackStepAndStyle(event); this.onSeekEnd(); }; handleMeasure = (name, { nativeEvent }) => { const { width, height } = 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.trackSize) { // console.warn('post return'); this.setState({ containerSize: this.containerSize, trackSize: this.trackSize, thumbSize: this.thumbSize }, () => { this.setState({ measureCompleted: true }); }); } }; onAccessibilityAction = event => { const { maximumValue, minimumValue, step } = this.props; const value = this.getValueForX(this._x); let newValue; switch (event.nativeEvent.actionName) { case 'increment': newValue = value !== maximumValue ? value + step : value; break; case 'decrement': newValue = value !== minimumValue ? value - step : value; break; default: newValue = value; break; } this._x = this.getXForValue(newValue); this.updateValue(this._x); this.updateStyles(this._x); _.invoke(AccessibilityInfo, 'announceForAccessibility', `New value ${newValue}`); }; /* Renders */ renderThumb = () => { const { thumbStyle, disabled, thumbTintColor, thumbHitSlop } = this.props; return <Animated.View hitSlop={thumbHitSlop} ref={this.setThumbRef} onLayout={this.onThumbLayout} {...this._panResponder.panHandlers} style={[styles.thumb, thumbStyle, { backgroundColor: disabled ? DEFAULT_COLOR : thumbTintColor || ACTIVE_COLOR }, { transform: [{ scale: this.state.thumbActiveAnimation }] }]} />; }; render() { const { containerStyle, trackStyle, renderTrack, disabled, disableRTL, minimumTrackTintColor = ACTIVE_COLOR, maximumTrackTintColor = DEFAULT_COLOR, testID } = this.props; return <View style={[styles.container, containerStyle]} onLayout={this.onContainerLayout} onAccessibilityAction={this.onAccessibilityAction} testID={testID} {...this.getAccessibilityProps()}> {_.isFunction(renderTrack) ? <View style={[styles.track, { backgroundColor: maximumTrackTintColor }, trackStyle]} onLayout={this.onTrackLayout}> {renderTrack()} </View> : <View> <View style={[styles.track, trackStyle, { backgroundColor: disabled ? INACTIVE_COLOR : maximumTrackTintColor }]} onLayout={this.onTrackLayout} /> <View ref={this.setMinTrackRef} style={[styles.track, trackStyle, styles.minimumTrack, Constants.isRTL && disableRTL && styles.trackDisableRTL, { backgroundColor: disabled ? DEFAULT_COLOR : minimumTrackTintColor }]} /> </View>} <View style={styles.touchArea} onTouchEnd={this.handleTrackPress} /> {this.renderThumb()} </View>; } } const styles = StyleSheet.create({ container: { height: THUMB_SIZE + SHADOW_RADIUS, justifyContent: 'center' }, track: { height: TRACK_SIZE, borderRadius: TRACK_SIZE / 2, overflow: 'hidden' }, minimumTrack: { position: 'absolute' }, trackDisableRTL: { right: 0 }, thumb: { position: 'absolute', width: THUMB_SIZE, height: THUMB_SIZE, borderRadius: THUMB_SIZE / 2, borderWidth: BORDER_WIDTH, borderColor: Colors.white, shadowColor: Colors.rgba(Colors.black, 0.3), shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.9, shadowRadius: SHADOW_RADIUS, elevation: 2 }, activeThumb: { width: THUMB_SIZE + 16, height: THUMB_SIZE + 16, borderRadius: (THUMB_SIZE + 16) / 2, borderWidth: BORDER_WIDTH }, touchArea: { ...StyleSheet.absoluteFillObject, backgroundColor: 'transparent' } });