@uiw/react-native
Version:
UIW for React Native
324 lines (323 loc) • 9.71 kB
JavaScript
import React, { Component } from 'react';
import { View, Easing, Animated, PanResponder, StyleSheet } from 'react-native';
const TRACK_SIZE = 4;
const THUMB_SIZE = 20;
const DEFAULT_ANIMATION_CONFIGS = {
spring: {
friction: 7,
tension: 100
},
timing: {
duration: 150,
easing: Easing.inOut(Easing.ease),
delay: 0
}
};
const getBoundedValue = ({
value,
maxValue,
minValue
}) => value > maxValue ? maxValue : value < minValue ? minValue : value;
export default class Slider extends Component {
_previousLeft = 0;
static defaultProps = {
value: 0,
step: 0,
maxValue: 1,
minValue: 0,
height: 6,
vertical: false,
shownThumb: true,
thumbSize: {
width: 20,
height: 20
},
maximumTrackTintColor: '#cacaca',
minimumTrackTintColor: '#008EF0',
thumbTintColor: '#fff',
animationType: 'timing',
onChange: () => {},
onSlidingComplete: () => {},
onSlidingStart: () => {}
};
constructor(props) {
super(props);
this.state = {
containerSize: {
width: 0,
height: 0
},
trackSize: {
width: 0,
height: 0
},
thumbSize: props.thumbSize || {
width: 0,
height: 0
},
value: new Animated.Value(getBoundedValue(props))
};
this.panResponder = PanResponder.create({
onPanResponderTerminationRequest: () => false,
onStartShouldSetPanResponder: () => true,
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => false,
onPanResponderGrant: this.handlePanResponderGrant,
onPanResponderMove: this.handlePanResponderMove,
onPanResponderRelease: this.handlePanResponderEnd,
onPanResponderTerminate: this.handlePanResponderEnd
});
}
componentDidUpdate(prevProps) {
const newValue = getBoundedValue(this.props);
if (prevProps.value !== newValue) {
if (this.props.animateTransitions) {
this.setCurrentValueAnimated(newValue);
} else {
this.setCurrentValue(newValue);
}
}
}
setCurrentValue(value) {
this.state.value.setValue(value);
}
setCurrentValueAnimated(value) {
const {
animationType
} = this.props;
if (!animationType) {
return;
}
const animationConfig = Object.assign({}, DEFAULT_ANIMATION_CONFIGS[animationType], this.props.animationConfig, {
toValue: value
});
Animated[animationType](this.state.value, animationConfig).start();
}
getThumbLeft(value) {
const ratio = (value - this.props.minValue) / (this.props.maxValue - this.props.minValue);
if (this.props.shownThumb) {
return ratio * (this.state.containerSize.width - this.state.thumbSize.width);
}
return ratio * this.state.containerSize.width;
}
getCurrentValue = () => this.state.value.__getValue();
handlePanResponderGrant = () => {
if (this.props.disabled) {
return;
}
this._previousLeft = this.getThumbLeft(this.getCurrentValue());
this.props.onSlidingStart(this.getCurrentValue());
};
handlePanResponderEnd = () => {
if (this.props.disabled) {
return;
}
this.props.onSlidingComplete(this.getCurrentValue());
};
handlePanResponderMove = (_, gestureState) => {
if (this.props.disabled) {
return;
}
const value = this.getValue(gestureState);
this.state.value.setValue(value);
this.props.onChange(this.getCurrentValue());
};
getValue(gestureState) {
const {
vertical,
minValue,
maxValue,
shownThumb
} = this.props;
let length = this.state.containerSize.width - this.state.thumbSize.width;
if (!shownThumb) {
length = this.state.containerSize.width;
}
const thumbLeft = this._previousLeft + (vertical ? gestureState.dy : gestureState.dx);
const ratio = thumbLeft / length;
if (this.props.step) {
return Math.max(minValue, Math.min(maxValue, minValue + Math.round(ratio * (maxValue - minValue) / this.props.step) * this.props.step));
}
return Math.max(minValue, Math.min(maxValue, ratio * (maxValue - minValue) + minValue));
}
handleMeasure(name, event) {
const {
width: layoutWidth,
height: layoutHeight
} = event.nativeEvent.layout;
const {
vertical
} = this.props;
const {
containerSize,
trackSize,
thumbSize
} = this.state;
const width = vertical ? layoutHeight : layoutWidth;
const height = vertical ? layoutWidth : layoutHeight;
const size = {
width,
height
};
const state = {
containerSize,
trackSize,
thumbSize
};
if (state[name]) {
state[name] = size;
}
this.setState({
...state
});
}
measureContainer = event => this.handleMeasure('containerSize', event);
measureThumb = event => this.handleMeasure('thumbSize', event);
getMinimumTrackStyles(thumbStart) {
const {
thumbSize,
trackSize
} = this.state;
const minimumTrackStyle = {
position: 'absolute'
};
if (this.props.vertical) {
minimumTrackStyle.height = Animated.add(thumbStart, 0);
// minimumTrackStyle.height = Animated.add(thumbStart, thumbSize.height / 2);
minimumTrackStyle.marginLeft = -trackSize.width;
} else {
minimumTrackStyle.width = Animated.add(thumbStart, 0);
// minimumTrackStyle.width = Animated.add(thumbStart, thumbSize.width / 2);
minimumTrackStyle.marginTop = -trackSize.height;
}
return minimumTrackStyle;
}
getThumbPositionStyles(thumbStart) {
if (this.props.vertical) {
return [{
translateX: -(this.state.trackSize.height + this.state.thumbSize.height) / 2
}, {
translateY: thumbStart
}];
}
return [{
translateX: thumbStart
}, {
translateY: -(this.state.trackSize.height + this.state.thumbSize.height) / 2
}];
}
render() {
const {
style,
vertical,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step,
maxValue,
minValue,
disabled,
shownThumb,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
animationType,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
animateTransitions,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
animationConfig,
trackStyle,
thumbStyle,
minimumTrackTintColor,
maximumTrackTintColor,
thumbTintColor,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onChange,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onSlidingComplete,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onSlidingStart,
...otherProps
} = this.props;
const {
value,
thumbSize,
containerSize
} = this.state;
const touchOverflowStyle = {};
let outputRange = containerSize.width;
if (shownThumb) {
outputRange = containerSize.width - thumbSize.width;
}
const thumbStart = value.interpolate({
inputRange: [minValue, maxValue],
outputRange: [0, outputRange]
// extrapolate: 'clamp',
});
const minimumTrackStyle = {
...this.getMinimumTrackStyles(thumbStart),
backgroundColor: disabled ? '#a0a5b1' : minimumTrackTintColor
};
const valueVisibleStyle = {};
const thumbStyleTransform = thumbStyle && thumbStyle.transform || [];
const thumbTrackStyle = {};
if (vertical) {
thumbTrackStyle.left = 16 + (trackStyle && trackStyle.width ? (trackStyle.width - 4) / 2 : 0);
touchOverflowStyle.height = '100%';
touchOverflowStyle.width = 30;
} else {
thumbTrackStyle.top = 16 + (trackStyle && trackStyle.height ? (trackStyle.height - 4) / 2 : 0);
touchOverflowStyle.height = 30;
}
return <View onLayout={this.measureContainer} {...otherProps} style={StyleSheet.flatten([vertical ? styles.containerVertical : styles.containerHorizontal, style])}>
<View style={StyleSheet.flatten([styles.track, vertical ? {
...styles.trackVertical,
height: '100%'
} : {
...styles.trackHorizontal,
width: '100%'
}, trackStyle, {
backgroundColor: maximumTrackTintColor,
position: 'absolute'
}])} />
<Animated.View style={StyleSheet.flatten([styles.track, vertical ? styles.trackVertical : styles.trackHorizontal, trackStyle, minimumTrackStyle])} />
{shownThumb && <Animated.View onLayout={this.measureThumb} style={StyleSheet.flatten([{
backgroundColor: thumbTintColor
}, styles.thumb, thumbTrackStyle, thumbStyle, {
transform: [...this.getThumbPositionStyles(thumbStart), ...thumbStyleTransform],
...valueVisibleStyle
}, {
width: vertical ? thumbSize.height : thumbSize.width,
height: vertical ? thumbSize.width : thumbSize.height,
borderRadius: thumbSize.width / 2
}])} />}
<View style={StyleSheet.flatten([styles.touchArea, touchOverflowStyle])} {...this.panResponder.panHandlers} />
</View>;
}
}
const styles = StyleSheet.create({
containerHorizontal: {
height: 30,
justifyContent: 'center'
},
containerVertical: {
width: 30,
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: {
backgroundColor: 'transparent'
}
});