UNPKG

tuya-panel-kit

Version:

a functional component library for developing tuya device panels!

687 lines (651 loc) 20.4 kB
/* eslint-disable indent */ import PropTypes from 'prop-types'; import React from 'react'; import Svg, { Path } from 'react-native-svg'; import { StyleSheet, View, ViewPropTypes } from 'react-native'; import Gesture from './gesture'; import PathCustom from './path-custom'; import Gradient from './gradient'; import ProgressCircle from './circle'; export default class ProgressDouble extends Gesture { static propTypes = { ...Gesture.propTypes, /** * 渐变ID */ gradientId: PropTypes.string, /** * 进度条样式 */ style: ViewPropTypes.style, /** * 最大具体值 */ maxValue: PropTypes.number, /** * 最小具体值 */ minValue: PropTypes.number, /** * 开始角度 */ startDegree: PropTypes.number, /** * 在开始的角度上增加的角度 */ andDegree: PropTypes.number, /** * 进度条始端最小值 */ min: PropTypes.number, /** * 进度条末端最大值 */ max: PropTypes.number, /** * 步长 */ stepValue: PropTypes.number, /** * 大于具体值的不透明度 */ backStrokeOpacity: PropTypes.number, /** * 小于具体值的不透明度 */ foreStrokeOpacity: PropTypes.number, /** * 进度条渲染的高度 */ scaleHeight: PropTypes.number, /** * 进度条是否可以手势滑动 */ disabled: PropTypes.bool, /** * 大于具体值的颜色 */ backColor: PropTypes.string, /** * 小于具体值的颜色 */ foreColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), /** * 值改变的回调 * @param {number} minValue - 最小具体值 * @param {number} maxValue - 最大具体值 */ onValueChange: PropTypes.func, /** * 滑动结束的回调 * @param {number} minValue - 最小具体值 * @param {number} maxValue - 最大具体值 */ onSlidingComplete: PropTypes.func, /** * 渐变起始点的x轴坐标 */ x1: PropTypes.string, /** * 渐变终点的x轴坐标 */ x2: PropTypes.string, /** * 渐变起始点的y轴坐标 */ y1: PropTypes.string, /** * 渐变终点的y轴坐标 */ y2: PropTypes.string, /** * 结束端thumb小圆球的填充色 */ thumbFill: PropTypes.string, /** * thumb小圆球边框宽度 */ thumbStrokeWidth: PropTypes.number, /** * 结束端thumb小圆球的边框色 */ thumbStroke: PropTypes.string, /** * thumb小圆球的半径 */ thumbRadius: PropTypes.number, /** * 开始端thumb小圆球的填充色 */ minThumbFill: PropTypes.string, /** * 开始端thumb小圆球的边框色 */ minThumbStroke: PropTypes.string, /** * 轨道不满360度开始的圆环颜色 */ startColor: PropTypes.string, /** * 轨道不满360度结束的圆环颜色 */ endColor: PropTypes.string, /** * 圆环中心自定义内容 */ renderCenterView: PropTypes.element, }; static defaultProps = { ...Gesture.defaultProps, gradientId: 'Double', maxValue: 25, minValue: 0, startDegree: 0, andDegree: 450, min: 0, max: 100, stepValue: 0, scaleHeight: 9, disabled: false, backColor: '#E5E5E5', foreColor: '#FF4800', onValueChange() {}, onSlidingComplete() {}, style: null, backStrokeOpacity: 1, foreStrokeOpacity: 1, x1: '0%', y1: '0%', x2: '100%', y2: '0%', thumbFill: '#fff', thumbStroke: '#FF4800', thumbStrokeWidth: 2, thumbRadius: 3.5, minThumbFill: '#fff', minThumbStroke: '#FF4800', startColor: '#E5E5E5', endColor: '#E5E5E5', renderCenterView: null, }; constructor(props) { super(props); this.fixDegreeAndBindToInstance(props); this.state = { minValue: props.minValue, maxValue: props.maxValue, }; } componentWillReceiveProps(nextProps) { this.fixDegreeAndBindToInstance(nextProps); if (this.state.minValue !== nextProps.minValue) { this.setState({ minValue: nextProps.minValue, }); } if (this.state.maxValue !== nextProps.maxValue) { this.setState({ maxValue: nextProps.maxValue, }); } } fixDegreeAndBindToInstance(props) { const { startDegree, andDegree, maxValue, minValue } = props; this.startDegree = startDegree % 360; if (andDegree >= 360) { this.andDegree = 360; } else { this.andDegree = andDegree; } if (startDegree !== 0 || !this.andDegree !== 0) { this.endDegree = (startDegree + this.andDegree) % 360 === 0 ? 360 : (startDegree + this.andDegree) % 360; } else { this.endDegree = 0; } // 基础圆环路径 this.backScalePath = this.createSvgPath(this.andDegree); const { progressStartX: startX, progressStartY: startY, progressX: endX, progressY: endY, } = this.getCirclePosition(this.backScalePath); this.startX = startX; this.startY = startY; this.endX = endX; this.endY = endY; // 具体值对应的角度 const deltaDeg = this.mapValueToDeltaDeg(maxValue, props); const minDeltaDeg = this.mapValueToDeltaDeg(minValue, props); // 小于具体值的路径 this.foreScalePath = this.createSvgPath(deltaDeg, minDeltaDeg); const { progressStartX, progressStartY, progressX, progressY } = this.getCirclePosition( this.foreScalePath ); this.progressX = progressX; this.progressY = progressY; this.progressStartY = progressStartY; this.progressStartX = progressStartX; } onStartShouldSetResponder({ nativeEvent: { locationX, locationY } }) { return this.shouldSetResponder(locationX, locationY); } shouldSetResponder(x0, y0) { const { scaleHeight, disabled, thumbRadius } = this.props; if (disabled) { return false; } const { r } = this.getCircleInfo(); const { x, y } = this.getXYRelativeCenter(x0, y0); const len = Math.sqrt( (x - thumbRadius) * (x - thumbRadius) + (y - thumbRadius) * (y - thumbRadius) ); const innerR = r - scaleHeight; const should = this.shouldUpdateScale(x0 - thumbRadius, y0 - thumbRadius); const finalShould = should && len <= r + thumbRadius && len >= innerR - thumbRadius; return finalShould; } shouldUpdateScale(x, y) { const { startDegree, endDegree } = this; const deg = this.getDegRelativeCenter(x, y); let should; if (endDegree <= startDegree) { should = (deg >= startDegree && deg > endDegree) || (deg < startDegree && deg <= endDegree); } else { should = (deg >= startDegree && deg < endDegree) || (deg > startDegree && deg <= endDegree); } return should; } onMoveShouldSetResponder() { return false; } onGrant(e, gestureState) { const { onValueChange } = this.props; this.eventHandle(gestureState, onValueChange); } onMove(e, gestureState) { const { onValueChange } = this.props; this.eventHandle(gestureState, onValueChange); } onRelease(e, gestureState) { const { onSlidingComplete } = this.props; this.eventHandle(gestureState, onSlidingComplete, true); } eventHandle({ locationX, locationY }, fn, isRelease = false) { const { startDegree, endDegree } = this; const { thumbRadius } = this.props; // 鼠标点击的坐标 const deg = this.getDegRelativeCenter(locationX - thumbRadius, locationY - thumbRadius); const isInArea = this.shouldUpdateScale(locationX - thumbRadius, locationY - thumbRadius); if (isInArea) { // 最小值对应的角度 const startDeg = this.getDegRelativeCenter(this.progressStartX, this.progressStartY); // 最大值对应的角度 const endDeg = this.getDegRelativeCenter(this.progressX, this.progressY); // 最小值距离基础圆环最小值的角度 const startToStart = this.startCompareToStart(startDegree, endDegree, startDeg); // 最大值距离基础圆环最小值的角度 const endToStart = endDeg >= startDegree ? endDeg - startDegree : 360 + endDeg - startDegree; const minValue = this.mapDeltaDegToValue(startToStart); const maxValue = this.mapDeltaDegToValue(endToStart); // 鼠标点击的位置与初始位置的角度 const deltaDegree = deg >= startDegree ? deg - startDegree : deg + 360 - startDegree; const value = this.mapDeltaDegToValue(deltaDegree); // 点击的角度与渲染圆环最小值的距离 const degToStartDeg = this.degCompareToStartDeg(deg, startDeg, startDegree); // 点击的角度与渲染圆环最大值的距离 const degToEndDeg = this.compareDeg(startDegree, endDegree, deg, endDeg); // 最大值与基础圆环最小值的角度 const endDegToStartDegree = endDeg >= startDegree ? endDeg - startDegree : 360 + endDeg - startDegree; // 最小值与基础圆环最小值的角度 const startDegToStartDegree = startDeg >= startDegree ? startDeg - startDegree : startDeg > endDegree ? startDegree - startDeg : 360 - startDegree + startDeg; if (degToStartDeg >= degToEndDeg) { this.foreScalePath = this.createSvgPath(deltaDegree, startDegToStartDegree); } else { this.foreScalePath = this.createSvgPath(endDegToStartDegree, deltaDegree); } const { progressStartX, progressStartY, progressX, progressY } = this.getCirclePosition( this.foreScalePath ); const locationToEnd = Math.sqrt( (this.progressX - (locationX - thumbRadius)) ** 2 + (this.progressY - (locationY - thumbRadius)) ** 2 ); const locationToStart = Math.sqrt( (this.progressStartX - (locationX - thumbRadius)) ** 2 + (this.progressStartY - (locationY - thumbRadius)) ** 2 ); if (locationToStart >= locationToEnd || value < minValue) { if (value < minValue) { this.progressStartX = progressStartX; this.progressStartY = progressStartY; if (typeof fn === 'function') fn({ minValue: value, maxValue }); this.setState({ minValue: value, maxValue, }); } else { this.progressX = progressX; this.progressY = progressY; if (typeof fn === 'function') fn({ minValue, maxValue: value }); this.setState({ minValue, maxValue: value, }); } } else if (locationToStart < locationToEnd || value > maxValue) { if (value > maxValue) { this.progressX = progressX; this.progressY = progressY; if (typeof fn === 'function') fn({ minValue, maxValue: value }); this.setState({ minValue, maxValue: value, }); } else { this.progressStartX = progressStartX; this.progressStartY = progressStartY; if (typeof fn === 'function') fn({ minValue: value, maxValue }); this.setState({ minValue: value, maxValue, }); } } } if (isRelease && !isInArea) { const { minValue, maxValue } = this.state; if (typeof fn === 'function') fn({ minValue, maxValue }); } } getCirclePosition = path => { const startIndex = path.indexOf(' A'); const progressStartIndex = path.indexOf(' '); const progressStartX = Number(path.substring(1, progressStartIndex)); const progressStartY = Number(path.substring(progressStartIndex + 1, startIndex)); const circleIndex = path.lastIndexOf(' 1 '); const needStr = path.substring(circleIndex + 3); const needIndex = needStr.indexOf(' '); const progressX = Number(needStr.substring(0, needIndex)); const progressY = Number(needStr.substring(needIndex + 1)); return { progressStartX, progressStartY, progressX, progressY }; }; getLayoutFromStyle(style) { const { width = 125, height = 125 } = StyleSheet.flatten(style) || {}; return { width, height, }; } // 获取圆环的半径信息 getCircleInfo() { const { width, height } = this.getLayoutFromStyle(this.props.style); const size = Math.min(width, height); const r = size / 2; const cx = r; const cy = r; return { r, cx, cy, }; } getXYRelativeCenter(x, y) { const { cx, cy } = this.getCircleInfo(); return { x: x - cx, y: y - cy, }; } getDegRelativeCenter(x, y) { const { x: _x, y: _y } = this.getXYRelativeCenter(x, y); let deg = (Math.atan2(_y, _x) * 180) / Math.PI; if (deg < 0) { deg += 360; } return parseInt(deg, 10); } // 进度条渲染线目的角度 mapDeltaDegToScaleCount(deltaDeg) { if (deltaDeg > this.andDegree) { return this.andDegree; } return deltaDeg; } startCompareToStart(startDegree, endDegree, startDeg) { // 当渲染的圆环的开始角度大于基础圆环的开始角度时 if (startDeg >= startDegree) { return startDeg - startDegree; } // 当基础圆环的开始角度大于渲染圆环的角度时 if (startDegree >= startDeg) { // 当基础圆环的开始角度大于渲染圆环的结束角度时 if (startDegree >= endDegree) { return 360 + startDeg - startDegree; } return startDegree - startDeg; } return 360 + startDeg - startDegree; } degCompareToStartDeg(deg, startDeg, startDegree) { // 当点击角度大于渲染圆环的开始角度 if (deg >= startDeg) { // 当渲染圆环的开始角度大于基础圆环的开始角度 if (startDeg >= startDegree) { return deg - startDeg; } // 当点击角度大于基础圆环的开始角度 if (deg >= startDegree) { return 360 - deg + startDeg; } return deg - startDeg; } // 当基础圆环的开始角度大于渲染圆环的开始角度 if (startDegree > startDeg) { return startDeg - deg; } // 当点击角度大于基础圆环的开始角度 if (deg >= startDegree) { return startDeg - deg; } return 360 - startDeg + deg; } compareDeg(startDegree, endDegree, deg, endDeg) { // 当基础圆环的结束角度大于开始角度时 if (endDegree > startDegree) { // 当前点击的角度大于渲染圆环的结束角度时 if (deg > endDeg) { return deg - endDeg; } return endDeg - deg; } // 当基础圆环的结束角度小于开始角度,当前点击的角度大于渲染圆环的结束角度时 if (deg > endDeg) { // 渲染圆环的结束角度大于基础圆环的结束角度时 if (endDeg > endDegree) { return deg - endDeg; } // 当前点击的角度小于基础圆环结束角度 if (deg < endDegree) { return deg - endDeg; } return 360 + endDeg - deg; } // 渲染圆环的结束角度大于基础圆环的结束角度 if (endDeg > endDegree) { // 当前点击的角度小于基础圆环的结束角度 if (deg < endDegree) { return 360 - endDeg + deg; } return endDeg - deg; } return endDeg - deg; } mapDeltaDegToValue(deltaDeg) { const angle = this.mapDeltaDegToScaleCount(deltaDeg); const { min, max, stepValue } = this.props; if (stepValue) { const deltaValue = (angle * (max - min)) / stepValue; const value = Math.round(deltaValue / this.andDegree); return Math.max(min, Math.min(max, value * stepValue + min)); } const deltaValue = max - min; const value = (angle * deltaValue) / this.andDegree; return Math.max(min, Math.min(max, value + min)); } // 具体值对应的角度 mapValueToDeltaDeg(value, props) { const { min, max } = props; return ((value - min) * this.andDegree) / (max - min); } // 计算路径路径 createSvgPath(deltaDeg = 0, minDeltaDeg = 0) { const { r } = this.getCircleInfo(); const { startDegree } = this; const { scaleHeight } = this.props; const innerRadius = r - scaleHeight; const countDegree = this.mapDeltaDegToScaleCount(deltaDeg); const endDegree = (countDegree + startDegree) % 360; const startAngle = (((startDegree + minDeltaDeg) % 360) * Math.PI) / 180; const endAngle = (endDegree * Math.PI) / 180; const _x1 = r + innerRadius * Math.cos(startAngle); const _y1 = r + innerRadius * Math.sin(startAngle); const _x2 = r + innerRadius * Math.cos(endAngle); const _y2 = r + innerRadius * Math.sin(endAngle); const num = countDegree - minDeltaDeg; if (countDegree - minDeltaDeg === 360) { const middleDegree = (this.mapDeltaDegToScaleCount(startDegree + minDeltaDeg + 180) * Math.PI) / 180; const middleX = r + innerRadius * Math.cos(middleDegree); const middleY = r + innerRadius * Math.sin(middleDegree); const path = `M${_x1} ${_y1} A${innerRadius} ${innerRadius} 0 ${ num > 180 ? (startDegree === 270 ? 0 : 1) : 0 } 1 ${middleX} ${middleY} A${innerRadius} ${innerRadius} 0 ${ num > 180 ? 1 : 0 } 1 ${_x2} ${_y2}`; return path; } const path = `M${_x1} ${_y1} A${innerRadius} ${innerRadius} 0 ${ num > 180 ? 1 : 0 } 1 ${_x2} ${_y2}`; return path; } render() { const responder = this.getResponder(); const { backColor, backStrokeOpacity, foreStrokeOpacity, foreColor, style, gradientId, scaleHeight, x1, x2, y1, y2, thumbFill, thumbStrokeWidth, thumbStroke, thumbRadius, minThumbFill, minThumbStroke, startColor, endColor, renderCenterView, } = this.props; const { r } = this.getCircleInfo(); const size = r * 2; const isGradient = foreColor && typeof foreColor === 'object'; return ( <View {...responder} style={[ style, { width: size + 2 * thumbRadius, height: size + 2 * thumbRadius, }, ]} > <Svg viewBox={`${-thumbRadius} ${-thumbRadius} ${size + 2 * thumbRadius} ${size + 2 * thumbRadius}`} width={size + 2 * thumbRadius} height={size + 2 * thumbRadius} > <Path d={this.backScalePath} x="0" y="0" fill="none" stroke={backColor} strokeWidth={scaleHeight} strokeOpacity={backStrokeOpacity} /> {this.andDegree < 360 && ( <ProgressCircle cx={this.startX} cy={this.startY} r={scaleHeight / 2 - 1} fill={startColor} stroke={startColor} /> )} {this.andDegree < 360 && ( <ProgressCircle cx={this.endX} cy={this.endY} r={scaleHeight / 2 - 1} fill={endColor} stroke={endColor} /> )} {isGradient && ( <Gradient gradientId={gradientId} x1={x1} x2={x2} y1={y1} y2={y2} isGradient={isGradient} foreColor={foreColor} /> )} <PathCustom isGradient={isGradient} path={this.foreScalePath} gradientId={gradientId} strokeOpacity={foreStrokeOpacity} strokeWidth={scaleHeight} foreColor={foreColor} /> <ProgressCircle cx={this.progressStartX} cy={this.progressStartY} r={thumbRadius} fill={minThumbFill} strokeWidth={thumbStrokeWidth} stroke={minThumbStroke} /> <ProgressCircle cx={this.progressX} cy={this.progressY} r={thumbRadius} fill={thumbFill} strokeWidth={thumbStrokeWidth} stroke={thumbStroke} /> </Svg> {renderCenterView} </View> ); } }