608 lines
23 KiB
JavaScript
608 lines
23 KiB
JavaScript
import React, { PureComponent } from 'react';
|
||
import { Animated, Image, StyleSheet, PanResponder, View, Text, Platform } from 'react-native';
|
||
import variables from '../../common/styles/variables';
|
||
import sliderStyles from './styles';
|
||
import Coord from './Coord';
|
||
const thumbImage = require('./images/rectangle.png');
|
||
const otherThumbImage = require('./images/rectangle.png');
|
||
const styles = StyleSheet.create(sliderStyles);
|
||
export default class Slider extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
/**
|
||
* 通过props获取滑块对应的value值
|
||
*/
|
||
this.getValueByProps = (isOther) => {
|
||
const { value, range } = this.props;
|
||
if (range && value instanceof Array) {
|
||
if (isOther) {
|
||
return value[1];
|
||
}
|
||
return value[0];
|
||
}
|
||
return value;
|
||
};
|
||
/**
|
||
* 判断用户触控的区域是否在滑块上
|
||
*/
|
||
this.thumbTouchCheck = (e) => {
|
||
const nativeEvent = e.nativeEvent;
|
||
const { range } = this.props;
|
||
if (range) {
|
||
const otherThumbCoord = this.getThumbCoord(range);
|
||
const otherCheckResult = otherThumbCoord.contain(nativeEvent.locationX, nativeEvent.locationY);
|
||
if (otherCheckResult) {
|
||
this.isOther = true;
|
||
return otherCheckResult;
|
||
}
|
||
}
|
||
const ThumbCoord = this.getThumbCoord();
|
||
const checkResult = ThumbCoord.contain(nativeEvent.locationX, nativeEvent.locationY);
|
||
if (checkResult) {
|
||
this.isOther = false;
|
||
return checkResult;
|
||
}
|
||
return false;
|
||
};
|
||
this.getThumbCoord = (isOther) => {
|
||
const { thumbSize, otherThumbSize, containerSize } = this.state;
|
||
let currThumb = thumbSize;
|
||
if (isOther) {
|
||
currThumb = otherThumbSize;
|
||
}
|
||
const { vertical } = this.props;
|
||
let x = null;
|
||
let y = null;
|
||
if (vertical) {
|
||
x = (containerSize.width - currThumb.width) / 2;
|
||
y = this.getThumbLeft(this.getCurrentValue(isOther));
|
||
}
|
||
else {
|
||
x = this.getThumbLeft(this.getCurrentValue(isOther));
|
||
y = (containerSize.height - currThumb.height) / 2;
|
||
}
|
||
return new Coord(x, y, currThumb.width, currThumb.height);
|
||
};
|
||
/**
|
||
* 滚动状态响应
|
||
*/
|
||
this.scroll = (gestureState) => {
|
||
if (this.props.disabled) {
|
||
return;
|
||
}
|
||
if (this.isOther) {
|
||
const isOtherValue = this.getValue(gestureState, this.isOther);
|
||
this.setCurrentValue(isOtherValue, this.isOther);
|
||
}
|
||
else {
|
||
const value = this.getValue(gestureState);
|
||
this.setCurrentValue(value);
|
||
}
|
||
};
|
||
this.touchStart = (e) => {
|
||
this.oldValue = this.state.value.__getValue();
|
||
this.oldOtherValue = this.state.otherValue.__getValue();
|
||
return this.thumbTouchCheck(e);
|
||
};
|
||
this.pressStart = () => {
|
||
if (this.isOther) {
|
||
this.otherPreviousLeft = this.getThumbLeft(this.getCurrentValue(this.isOther));
|
||
}
|
||
else {
|
||
this.previousLeft = this.getThumbLeft(this.getCurrentValue());
|
||
}
|
||
};
|
||
this.lastMove = (_, gestureState) => {
|
||
this.scroll(gestureState);
|
||
};
|
||
this.touchEnd = (_, gestureState) => {
|
||
this.scroll(gestureState);
|
||
if (this.oldValue !== this.getCurrentValue()) {
|
||
this.triggerEvent('onChange');
|
||
}
|
||
if (this.props.range && this.oldOtherValue !== this.getCurrentValue(true)) {
|
||
this.triggerEvent('onChange');
|
||
}
|
||
};
|
||
this.measureContainer = (x) => {
|
||
this.handleMeasure('containerSize', x);
|
||
};
|
||
this.measureTrack = (x) => {
|
||
this.handleMeasure('trackSize', x);
|
||
};
|
||
this.measureThumb = (x) => {
|
||
this.handleMeasure('thumbSize', x);
|
||
};
|
||
this.measureOtherThumb = (x) => {
|
||
this.handleMeasure('otherThumbSize', x);
|
||
};
|
||
this.handleMeasure = (name, x) => {
|
||
const { width, height } = x.nativeEvent.layout;
|
||
const size = { width, height };
|
||
const currentSize = this.state[name];
|
||
if (currentSize &&
|
||
width === currentSize.width &&
|
||
height === currentSize.height) {
|
||
return;
|
||
}
|
||
const newState = {
|
||
[name]: size
|
||
};
|
||
// 双滑块
|
||
this.setState(newState);
|
||
};
|
||
/**
|
||
* 获取可滑动长度
|
||
*/
|
||
this.getScrollLength = () => {
|
||
const { vertical } = this.props;
|
||
const { trackSize } = this.state;
|
||
if (vertical) {
|
||
return trackSize.height;
|
||
}
|
||
else {
|
||
return trackSize.width;
|
||
}
|
||
};
|
||
/**
|
||
* 获取滑块的坐标的宽度
|
||
* 如果是横向slider则取width,纵向取height
|
||
*/
|
||
this.getThumbOffset = (isOther) => {
|
||
const { vertical } = this.props;
|
||
const { thumbSize, otherThumbSize } = this.state;
|
||
if (vertical && isOther) {
|
||
return otherThumbSize.height;
|
||
}
|
||
else if (!vertical && isOther) {
|
||
return otherThumbSize.width;
|
||
}
|
||
else if (vertical && !isOther) {
|
||
return thumbSize.height;
|
||
}
|
||
else if (!vertical && !isOther) {
|
||
return thumbSize.height;
|
||
}
|
||
};
|
||
/**
|
||
* 获取当前value值所占的百分比
|
||
*/
|
||
this.getRatio = (value) => {
|
||
const { min, max } = this.props;
|
||
if (max === min) {
|
||
return 0;
|
||
}
|
||
return (value - min) / (max - min);
|
||
};
|
||
/**
|
||
* 滑块在滑动轴上的偏移量
|
||
* value => x
|
||
*/
|
||
this.getThumbLeft = (value) => {
|
||
const { vertical } = this.props;
|
||
let ratio = this.getRatio(value);
|
||
if (vertical) {
|
||
ratio = 1 - ratio;
|
||
}
|
||
const scrollLength = this.getScrollLength();
|
||
return (ratio * scrollLength);
|
||
};
|
||
/**
|
||
* 互斥prop
|
||
* 刻度属性只有正在非纵向轴、非双滑块下才生效
|
||
*/
|
||
this.showStep = () => {
|
||
const { vertical, step, range } = this.props;
|
||
if (!range && !vertical && step) {
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
/**
|
||
* 获取滑动位置所对应的value值,和getThumbLeft方法对应
|
||
* x => value
|
||
*/
|
||
this.getValue = (gestureState, isOther) => {
|
||
let previousLeft = this.previousLeft;
|
||
if (isOther) {
|
||
previousLeft = this.otherPreviousLeft;
|
||
}
|
||
const scrollLength = this.getScrollLength();
|
||
const { step, min, max, vertical } = this.props;
|
||
let thumbLeft = null;
|
||
if (vertical) {
|
||
thumbLeft = previousLeft + gestureState.dy;
|
||
}
|
||
else {
|
||
thumbLeft = previousLeft + gestureState.dx;
|
||
}
|
||
let ratio = thumbLeft / scrollLength;
|
||
if (vertical) {
|
||
ratio = 1 - ratio;
|
||
}
|
||
if (this.showStep()) {
|
||
return Math.max(min, Math.min(max, min + Math.round(ratio * (max - min) / step) * step));
|
||
}
|
||
return Math.max(min, Math.min(max, ratio * (max - min) + min));
|
||
};
|
||
/**
|
||
* 获取滑块的value值
|
||
*/
|
||
this.getCurrentValue = (isOther) => {
|
||
let value = this.state.value;
|
||
if (isOther) {
|
||
value = this.state.otherValue;
|
||
}
|
||
return value.__getValue();
|
||
};
|
||
this.setCurrentValue = (value, isOther) => {
|
||
if (isOther) {
|
||
this.setState({
|
||
otherTip: `${Math.round(value)}`
|
||
});
|
||
this.state.otherValue.setValue(value);
|
||
}
|
||
else {
|
||
this.setState({
|
||
tip: `${Math.round(value)}`
|
||
});
|
||
this.state.value.setValue(value);
|
||
}
|
||
};
|
||
this.triggerEvent = (event) => {
|
||
if (this.props[event]) {
|
||
let args = [Math.round(this.getCurrentValue())];
|
||
const { range } = this.props;
|
||
if (range) {
|
||
if (this.compareValue()) {
|
||
args.unshift(Math.round(this.getCurrentValue(range)));
|
||
}
|
||
else {
|
||
args.push(Math.round(this.getCurrentValue(range)));
|
||
}
|
||
this.props[event](args);
|
||
}
|
||
else {
|
||
this.props[event](args[0]);
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* 默认滑块的的滑块图片渲染
|
||
*/
|
||
this.renderThumbImage = (isOther) => {
|
||
if (!isOther && !thumbImage)
|
||
return;
|
||
if (isOther && !otherThumbImage)
|
||
return;
|
||
const { renderThumb } = this.props;
|
||
const { thumbSize } = this.state;
|
||
if (typeof renderThumb === 'function') {
|
||
return renderThumb(isOther);
|
||
}
|
||
return React.createElement(Image, { style: [{width:thumbSize.width-20,height:thumbSize.height-50}, {translateX:thumbSize.width/2 - 10,position:"absolute",bottom:0, borderRadius: this.getThumbOffset() / 2 }], source: isOther ? otherThumbImage : thumbImage });
|
||
};
|
||
/**
|
||
* 刻度模块的渲染
|
||
*/
|
||
this.renderMarks = () => {
|
||
const { step, marks, min, max, thumbSize } = this.props;
|
||
if (!this.showStep() || !marks) {
|
||
return null;
|
||
}
|
||
const maxStep = Math.ceil(Math.abs((max - min) / step)) + 1;
|
||
let currStep = 0;
|
||
const markViewArr = [];
|
||
while (maxStep > currStep) {
|
||
if (React.isValidElement(marks[currStep])) {
|
||
markViewArr.push(marks[currStep]);
|
||
}
|
||
else {
|
||
markViewArr.push(React.createElement(View, { key: currStep, style: { width: thumbSize, alignItems: 'center' } },
|
||
React.createElement(Text, { style: styles.markItemText }, marks[currStep]),
|
||
React.createElement(View, { style: styles.markItemLine })));
|
||
}
|
||
currStep += 1;
|
||
}
|
||
return (React.createElement(View, { style: styles.markContainer }, markViewArr));
|
||
};
|
||
/**
|
||
* 渲染滑块的toopTip提示
|
||
*/
|
||
this.renderThumbToolTip = (isOther) => {
|
||
const { showTip,thumbSize, renderTip } = this.props;
|
||
if (!showTip) {
|
||
return;
|
||
}
|
||
const { tip, otherTip } = this.state;
|
||
return (React.createElement(View, { style: [
|
||
styles.tip,
|
||
{ bottom:20 + (thumbSize - 30) / 2}
|
||
] },
|
||
React.createElement(View, { key: 1, style: styles.tipContent }, renderTip ? renderTip(isOther ? otherTip : tip)
|
||
:
|
||
React.createElement(Text, { style: styles.tipText }, isOther ? otherTip : tip)),
|
||
React.createElement(View, { key: 2, style: styles.tipIcon })));
|
||
};
|
||
/**
|
||
* 滑动的起始和结束x值
|
||
*/
|
||
this.getScrollRange = () => {
|
||
const scrollLength = this.getScrollLength();
|
||
return [0, scrollLength];
|
||
};
|
||
/**
|
||
* 滑块渲染
|
||
*/
|
||
this.renderThumb = (isOther) => {
|
||
const { vertical, range } = this.props;
|
||
if (isOther && !range)
|
||
return;
|
||
const { value, otherValue, thumbSize, otherThumbSize } = this.state;
|
||
let currValue = value;
|
||
let currThumb = thumbSize;
|
||
let measureFn = this.measureThumb;
|
||
if (isOther) {
|
||
currValue = otherValue;
|
||
currThumb = otherThumbSize;
|
||
measureFn = this.measureOtherThumb;
|
||
}
|
||
|
||
const thumbLeft = this.getThumbLeft(currValue.__getValue());
|
||
let thumbStyle = {
|
||
transform: [{ translateX: thumbLeft+7.5 + (currThumb.width - 20) / 2 }, { translateY: -20 + (currThumb.width -50) / 2 }],
|
||
alignItems: 'center',
|
||
};
|
||
if (vertical) {
|
||
thumbStyle.transform = [{ translateX: 0 }, { translateY: thumbLeft }];
|
||
}
|
||
if (this.showAndroidTip) {
|
||
return (React.createElement(Animated.View, { onLayout: measureFn, renderToHardwareTextureAndroid: true, style: [
|
||
styles.thumb,
|
||
thumbStyle,
|
||
] },
|
||
this.renderThumbToolTip(isOther),
|
||
React.createElement(View, { style: [
|
||
currThumb,
|
||
{ }
|
||
] }, this.renderThumbImage(isOther))));
|
||
}
|
||
return (React.createElement(Animated.View, { onLayout: measureFn, renderToHardwareTextureAndroid: true, style: [
|
||
styles.thumb,
|
||
currThumb,
|
||
thumbStyle,
|
||
] },
|
||
this.renderThumbToolTip(isOther),
|
||
this.renderThumbImage(isOther)));
|
||
};
|
||
/**
|
||
* 两个滑块值比较,滑块A的值是否大于B
|
||
*/
|
||
this.compareValue = () => {
|
||
const { range } = this.props;
|
||
return range && this.getCurrentValue() >= this.getCurrentValue(true);
|
||
};
|
||
/**
|
||
* 滑轨色值计算
|
||
* 双滑块模式下,需要根据两个滑块的值大小结果互换色值
|
||
* 垂直滑块模式下,因为滑块的渲染是从顶部计算的,所以滑块需要使用反向色值来实现从底部滑动的效果
|
||
* 假设滑块A,B
|
||
*/
|
||
this.getTrackColor = (isOther) => {
|
||
const { minTrackColor, midTrackColor, maxTrackColor, range, vertical } = this.props;
|
||
let activeColor = '';
|
||
// 双滑块B
|
||
if (isOther) {
|
||
if (this.compareValue()) {
|
||
if (vertical) {
|
||
// 纵向双滑块A>=B B => midTrackColor
|
||
activeColor = midTrackColor;
|
||
}
|
||
else {
|
||
// 横向双滑块A>=B B => minTrackColor
|
||
activeColor = minTrackColor;
|
||
}
|
||
}
|
||
else {
|
||
if (vertical) {
|
||
// 纵向双滑块A<B B => trackColor
|
||
activeColor = maxTrackColor;
|
||
}
|
||
else {
|
||
// 横向双滑块A<B B => midTrackColor
|
||
activeColor = midTrackColor;
|
||
}
|
||
}
|
||
// 双滑块A
|
||
}
|
||
else if (range) {
|
||
if (this.compareValue()) {
|
||
if (vertical) {
|
||
// 纵向双滑块A<B A => trackColor
|
||
activeColor = maxTrackColor;
|
||
}
|
||
else {
|
||
// 横向双滑块A<B A => midTrackColor
|
||
activeColor = midTrackColor;
|
||
}
|
||
}
|
||
else {
|
||
if (vertical) {
|
||
// 纵向双滑块A>=B A => midTrackColor
|
||
activeColor = midTrackColor;
|
||
}
|
||
else {
|
||
// 横向单滑块A>=B A => minTrackColor
|
||
activeColor = minTrackColor;
|
||
}
|
||
}
|
||
// 单滑块
|
||
}
|
||
else {
|
||
if (vertical) {
|
||
// 纵向单滑块 A => trackColor
|
||
activeColor = maxTrackColor;
|
||
}
|
||
else {
|
||
// 横向单滑块 A => minTrackColor
|
||
activeColor = minTrackColor;
|
||
}
|
||
}
|
||
return [activeColor];
|
||
};
|
||
/**
|
||
* 默认滑块划过的滑轨
|
||
*/
|
||
this.renderMinimumTrack = (isOther) => {
|
||
const { disabled, range, vertical, trackWeight } = this.props;
|
||
if (isOther && !range)
|
||
return;
|
||
const { value, otherValue } = this.state;
|
||
let currValue = value;
|
||
let minimumTrackColor = null;
|
||
let currKey = 'minTrack';
|
||
if (isOther) {
|
||
currValue = otherValue;
|
||
currKey = 'otherMinTrack';
|
||
}
|
||
const minimumTrackWidth = this.getThumbLeft(currValue.__getValue());
|
||
// 滑轨颜色值设定
|
||
const trackColors = this.getTrackColor(isOther);
|
||
minimumTrackColor = trackColors[0];
|
||
const minimumTrackStyle = {
|
||
position: 'absolute',
|
||
backgroundColor: minimumTrackColor
|
||
};
|
||
let trackStyle = null;
|
||
if (vertical) {
|
||
minimumTrackStyle.height = minimumTrackWidth;
|
||
minimumTrackStyle.width = this.props.trackWeight;
|
||
trackStyle = { marginVertical: this.getThumbOffset(isOther) / 2 };
|
||
}
|
||
else {
|
||
minimumTrackStyle.height = this.props.trackWeight;
|
||
minimumTrackStyle.width = minimumTrackWidth;
|
||
trackStyle = { marginHorizontal: this.getThumbOffset(isOther) / 2 };
|
||
}
|
||
return (React.createElement(Animated.View, { key: currKey, renderToHardwareTextureAndroid: true, style: [{ borderRadius: trackWeight / 2 }, minimumTrackStyle, trackStyle] }));
|
||
};
|
||
this.getTrackStyle = () => {
|
||
const { range, vertical, maxTrackColor, minTrackColor, trackWeight, thumbSize } = this.props;
|
||
const trackStyle = { backgroundColor: maxTrackColor };
|
||
let marginArr = ['marginLeft', 'marginRight', 'marginHorizontal', 'height'];
|
||
const rest = thumbSize - trackWeight;
|
||
const spacing = rest > 0 ? Math.ceil(rest / 2) : 0;
|
||
if (vertical) {
|
||
marginArr = ['marginTop', 'marginBottom', 'marginVertical', 'width'];
|
||
trackStyle.flex = 1;
|
||
trackStyle.alignItems = 'flex-start';
|
||
trackStyle.backgroundColor = minTrackColor;
|
||
trackStyle.marginHorizontal = spacing;
|
||
}
|
||
else {
|
||
trackStyle.marginVertical = spacing;
|
||
}
|
||
// 样式处理
|
||
if (range) {
|
||
trackStyle[marginArr[0]] = this.getThumbOffset() / 2;
|
||
trackStyle[marginArr[1]] = this.getThumbOffset(true) / 2;
|
||
}
|
||
else {
|
||
trackStyle[marginArr[2]] = this.getThumbOffset() / 2;
|
||
}
|
||
trackStyle[marginArr[3]] = this.props.trackWeight;
|
||
return trackStyle;
|
||
};
|
||
this.renderTracks = () => {
|
||
const { vertical, trackWeight } = this.props;
|
||
const trackStyle = this.getTrackStyle();
|
||
// 如果value > oldValue,则代表两个滑块滑动位置互换,则更新渲染层级
|
||
let tracks = [
|
||
React.createElement(View, { style: [{ borderRadius: trackWeight / 2 }, trackStyle], onLayout: this.measureTrack })
|
||
];
|
||
// vertical的值和this.compareValue()值相同时,次滑块轴在底层,反之主滑块轴在底层
|
||
if (vertical === this.compareValue()) {
|
||
tracks.push(this.renderMinimumTrack(true));
|
||
tracks.push(this.renderMinimumTrack());
|
||
}
|
||
else {
|
||
tracks.push(this.renderMinimumTrack());
|
||
tracks.push(this.renderMinimumTrack(true));
|
||
}
|
||
return tracks;
|
||
};
|
||
this.state = {
|
||
containerSize: { width: 0, height: 0 },
|
||
trackSize: { width: 0, height: 0 },
|
||
thumbSize: { width: props.thumbSize, height: props.thumbSize },
|
||
otherThumbSize: { width: props.thumbSize, height: props.thumbSize },
|
||
value: new Animated.Value(this.getValueByProps()),
|
||
otherValue: new Animated.Value(this.getValueByProps(true)),
|
||
tip: `${this.getValueByProps()}`,
|
||
otherTip: `${this.getValueByProps(true)}`
|
||
};
|
||
this.isOther = false;
|
||
this.showAndroidTip = this.props.showTip && Platform.OS === 'android';
|
||
}
|
||
componentWillMount() {
|
||
this.panResponder = PanResponder.create({
|
||
onStartShouldSetPanResponder: this.touchStart,
|
||
onMoveShouldSetPanResponder: _ => false,
|
||
onPanResponderGrant: this.pressStart,
|
||
onPanResponderMove: this.lastMove,
|
||
onPanResponderRelease: this.touchEnd,
|
||
onPanResponderTerminationRequest: _ => false,
|
||
onPanResponderTerminate: this.touchEnd
|
||
});
|
||
}
|
||
componentWillReceiveProps(nextProps) {
|
||
let newValue = 0;
|
||
let newOtherValue = 0;
|
||
const { range } = this.props;
|
||
if (range && nextProps.value instanceof Array) {
|
||
newValue = nextProps.value[0];
|
||
newOtherValue = nextProps.value[1];
|
||
}
|
||
else {
|
||
newValue = nextProps.value;
|
||
}
|
||
if (this.getValueByProps() !== newValue) {
|
||
this.setCurrentValue(newValue);
|
||
}
|
||
if (range && this.getValueByProps(true) !== newOtherValue) {
|
||
this.setCurrentValue(newOtherValue, range);
|
||
}
|
||
}
|
||
render() {
|
||
const { style, vertical } = this.props;
|
||
return (React.createElement(View, { style: [
|
||
{ flexDirection: vertical ? 'row' : 'column' },
|
||
style
|
||
] },
|
||
this.renderMarks(),
|
||
React.createElement(View, { style: {
|
||
alignItems: vertical ? 'center' : undefined,
|
||
justifyContent: vertical ? undefined : 'center'
|
||
}, onLayout: this.measureContainer },
|
||
this.renderTracks(),
|
||
this.renderThumb(),
|
||
this.renderThumb(true),
|
||
React.createElement(View, Object.assign({ renderToHardwareTextureAndroid: true, style: styles.touchArea }, this.panResponder.panHandlers)))));
|
||
}
|
||
}
|
||
Slider.defaultProps = {
|
||
value: 0,
|
||
min: 0,
|
||
max: 1,
|
||
step: 0,
|
||
maxTrackColor: variables.mtdFillGray,
|
||
minTrackColor: variables.mtdBrandPrimary,
|
||
midTrackColor: variables.mtdBrandDanger,
|
||
range: false,
|
||
vertical: false,
|
||
showTip: false,
|
||
trackWeight: 5,
|
||
thumbSize: 30
|
||
};
|
||
//# sourceMappingURL=Slider.js.map
|