UNPKG

tuya-panel-kit

Version:

a functional component library for developing tuya device panels!

643 lines (619 loc) 18.3 kB
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 ProgressCircle from './circle'; export default class ProgressSimple extends Gesture { static propTypes = { ...Gesture.propTypes, /** * 进度条样式 */ style: ViewPropTypes.style, /** * 具体值1 */ value1: PropTypes.number, /** * 具体值2 */ value2: PropTypes.number, /** * 进度条1开始角度 */ startDegree1: PropTypes.number, /** * 进度条1在开始的角度上增加的角度 */ andDegree1: PropTypes.number, /** * 进度条2开始角度 */ startDegree2: PropTypes.number, /** * 进度条2在开始的角度上减少的角度 */ reduceDegree2: PropTypes.number, /** * 进度条1最小值 */ min1: PropTypes.number, /** * 进度条1最大值 */ max1: PropTypes.number, /** * 进度条2最小值 */ min2: PropTypes.number, /** * 进度条2最大值 */ max2: PropTypes.number, /** * 步长 */ stepValue: PropTypes.number, /** * 大于具体值的不透明度 */ backStrokeOpacity: PropTypes.number, /** * 小于具体值的不透明度 */ foreStrokeOpacity: PropTypes.number, /** * 进度条1渲染的高度 */ scaleHeight1: PropTypes.number, /** * 进度条2渲染的高度 */ scaleHeight2: PropTypes.number, /** * 进度条是否可以手势滑动 */ disabled: PropTypes.bool, /** * 进度条大于具体值的颜色 */ backColor: PropTypes.string, /** * 进度条小于具体值的颜色 */ foreColor: PropTypes.string, /** * 值改变的回调 * @param {number} value1 - 具体值1 * @param {number} value2 - 具体值2 */ onValueChange: PropTypes.func, /** * 滑动结束的回调 * @param {number} value1 - 具体值1 * @param {number} value2 - 具体值2 */ onSlidingComplete: PropTypes.func, /** * Thumb小圆球的填充色 */ thumbFill: PropTypes.string, /** * Thumb小圆球边框宽度 */ thumbStrokeWidth: PropTypes.number, /** * Thumb小圆球的边框色 */ thumbStroke: PropTypes.string, /** * 进度条1Thumb小圆球的半径 */ thumbRadius1: PropTypes.number, /** * 进度条2Thumb小圆球的半径 */ thumbRadius2: PropTypes.number, /** * 是否需要最大值的Touch */ needCircle1: PropTypes.bool, /** * 是否需要另一个轨道上的thumb */ needCircle2: PropTypes.bool, /** * 轨道开始的圆环颜色 */ startColor: PropTypes.string, /** * 轨道结束的圆环颜色 */ endColor: PropTypes.string, /** * 进度条2 Thumb小圆球的填充色 */ thumbFill2: PropTypes.string, /** * 进度条2 Thumb小圆球边框宽度 */ thumbStrokeWidth2: PropTypes.number, /** * 进度条2 Thumb小圆球的边框色 */ thumbStroke2: PropTypes.string, }; static defaultProps = { ...Gesture.defaultProps, value1: 50, value2: 20, startDegree1: 165, andDegree1: 215, startDegree2: 140, reduceDegree2: 100, min1: 0, max1: 100, min2: 0, max2: 50, stepValue: 0, scaleHeight1: 9, scaleHeight2: 4, disabled: false, backColor: '#E5E5E5', foreColor: '#FF4800', onValueChange() {}, onSlidingComplete() {}, style: null, backStrokeOpacity: 1, foreStrokeOpacity: 1, thumbFill: '#fff', thumbStroke: '#fff', thumbStrokeWidth: 2, thumbRadius1: 5, thumbRadius2: 2, needCircle1: true, needCircle2: true, startColor: '#FF4800', endColor: '#E5E5E5', thumbFill2: '#fff', thumbStrokeWidth2: 2, thumbStroke2: '#fff', }; constructor(props) { super(props); this.fixDegreeAndBindToInstance(props); this.state = { value1: props.value1, value2: props.value2, }; } componentWillReceiveProps(nextProps) { this.fixDegreeAndBindToInstance(nextProps); if (this.state.value1 !== nextProps.value1) { this.setState({ value1: nextProps.value1, }); } if (this.state.value2 !== nextProps.value2) { this.setState({ value2: nextProps.value2, }); } } fixDegreeAndBindToInstance(props) { const { startDegree1, andDegree1, value1, startDegree2, reduceDegree2, value2 } = props; this.startDegree1 = startDegree1 % 360; this.startDegree2 = startDegree2 % 360; if (andDegree1 >= 360) { this.andDegree1 = 360; } else { this.andDegree1 = andDegree1; } if (reduceDegree2 >= 360) { this.andDegree2 = 360; } else { this.andDegree2 = reduceDegree2; } if (startDegree1 !== 0 || !this.andDegree1 !== 0) { this.endDegree1 = (startDegree1 + this.andDegree1) % 360 === 0 ? 360 : (startDegree1 + this.andDegree1) % 360; } else { this.endDegree1 = 0; } if (startDegree2 !== 0 || !this.andDegree2 !== 0) { this.endDegree2 = startDegree2 - this.andDegree2 >= 0 ? startDegree2 - this.andDegree2 : 360 - startDegree2 + this.andDegree2; } else { this.endDegree2 = 0; } // 基础圆环路径1 this.backScalePath1 = this.createSvgPath(this.andDegree1, true); const { progressStartX: startX1, progressStartY: startY1, progressX: endX1, progressY: endY1, } = this.getCirclePosition(this.backScalePath1); this.startX1 = startX1; this.startY1 = startY1; this.endX1 = endX1; this.endY1 = endY1; // 基础圆环路径 this.backScalePath2 = this.createSvgPath(this.andDegree2, false); const { progressStartX: startX2, progressStartY: startY2, progressX: endX2, progressY: endY2, } = this.getCirclePosition(this.backScalePath2, false); this.startX2 = startX2; this.startY2 = startY2; this.endX2 = endX2; this.endY2 = endY2; // 具体值对应的角度 const deltaDeg1 = this.mapValueToDeltaDeg(value1, true, props); // 小于具体值的路径 this.foreScalePath1 = this.createSvgPath(deltaDeg1); // 具体值对应的角度 const deltaDeg2 = this.mapValueToDeltaDeg(value2, false, props); // 小于具体值的路径 this.foreScalePath2 = this.createSvgPath(deltaDeg2, false); const { progressX: progressX1, progressY: progressY1 } = this.getCirclePosition( this.foreScalePath1 ); const { progressX: progressX2, progressY: progressY2 } = this.getCirclePosition( this.foreScalePath2, false ); this.progressX1 = progressX1; this.progressY1 = progressY1; this.progressX2 = progressX2; this.progressY2 = progressY2; } onStartShouldSetResponder({ nativeEvent: { locationX, locationY } }) { return this.shouldSetResponder(locationX, locationY); } shouldSetResponder(x0, y0) { const { scaleHeight1, scaleHeight2, disabled, thumbRadius1, thumbRadius2 } = this.props; if (disabled) { return false; } const { r } = this.getCircleInfo(); const { x, y } = this.getXYRelativeCenter(x0, y0); const view = thumbRadius1 > thumbRadius2 ? thumbRadius1 : thumbRadius2; const scaleHeight = scaleHeight1 > scaleHeight2 ? scaleHeight1 : scaleHeight2; const len = Math.sqrt((x - view) * (x - view) + (y - view) * (y - view)); const innerR = r - scaleHeight; const should = this.shouldUpdateScale(x0, y0); const finalShould = should && len <= r + view && len >= innerR - view; return finalShould; } shouldUpdateScale(x, y) { const { startDegree1, startDegree2, endDegree1, endDegree2 } = this; const deg = this.getDegRelativeCenter(x, y); let should; if ((deg < startDegree1 && deg > startDegree2) || (deg < endDegree2 && deg > endDegree1)) { should = false; } else { should = true; } 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 { startDegree1, endDegree1, startDegree2 } = this; const { needCircle1, needCircle2, value1, value2 } = this.props; const deg = this.getDegRelativeCenter(locationX, locationY); const compareDeg = endDegree1 >= startDegree1 ? deg >= startDegree1 && deg <= endDegree1 : deg >= startDegree1 || deg <= endDegree1; const isInArea = this.shouldUpdateScale(locationX, locationY); if (isInArea) { let deltaDeg; if (compareDeg) { deltaDeg = deg - startDegree1; if (deltaDeg < 0) { deltaDeg = deg + 360 - startDegree1; } } else { deltaDeg = startDegree2 - deg; if (deltaDeg < 0) { deltaDeg = deg - 360 + startDegree2; } } const path = this.createSvgPath(deltaDeg, compareDeg); const { progressX, progressY } = this.getCirclePosition(path, compareDeg); if (compareDeg) { this.foreScalePath1 = path; if (needCircle1) { this.progressX1 = progressX; this.progressY1 = progressY; } const value = this.mapDeltaDegToValue(deltaDeg, true); if (typeof fn === 'function') fn({ value1: value, value2 }); this.setState({ value1: value, value2, }); } else { this.foreScalePath2 = path; if (needCircle2) { this.progressX2 = progressX; this.progressY2 = progressY; } const value = this.mapDeltaDegToValue(deltaDeg, false); if (typeof fn === 'function') fn({ value1, value2: value }); this.setState({ value1, value2: value, }); } } if (isRelease && !isInArea) { const { value1: stateValue1, value2: stateValue2 } = this.state; if (typeof fn === 'function') fn({ value1: stateValue1, value2: stateValue2 }); } } getCirclePosition = (path, back = true) => { 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 = back ? path.lastIndexOf(' 1 ') : path.lastIndexOf(' 0 '); 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 { thumbRadius1, thumbRadius2 } = this.props; const view = thumbRadius1 > thumbRadius2 ? thumbRadius1 : thumbRadius2; const { x: _x, y: _y } = this.getXYRelativeCenter(x - view, y - view); let deg = (Math.atan2(_y, _x) * 180) / Math.PI; if (deg < 0) { deg += 360; } return parseInt(deg, 10); } // 进度条渲染线目的角度 mapDeltaDegToScaleCount(deltaDeg, back = true) { if (back) { if (deltaDeg >= this.andDegree1) { return this.andDegree1; } return deltaDeg; } if (deltaDeg >= this.andDegree2) { return this.andDegree2; } return deltaDeg; } mapDeltaDegToValue(deltaDeg, bool) { const angle = this.mapDeltaDegToScaleCount(deltaDeg); const { min1, max1, min2, max2, stepValue } = this.props; if (bool) { if (stepValue) { const deltaValue = max1 - min1; const value = Math.round((angle * deltaValue) / stepValue / this.andDegree1); return Math.max(min1, Math.min(max1, value * stepValue + min1)); } const deltaValue = max1 - min1; const value = (angle * deltaValue) / this.andDegree1; return Math.max(min1, Math.min(max1, value + min1)); } if (stepValue) { const deltaValue = max2 - min2; const value = Math.round((angle * deltaValue) / stepValue / this.andDegree2); return Math.max(min2, Math.min(max2, value * stepValue + min2)); } const deltaValue = max2 - min2; const value = (angle * deltaValue) / this.andDegree2; return Math.max(min2, Math.min(max2, value + min2)); } // 具体值对应的角度 mapValueToDeltaDeg(value, bool, props) { const { min1, max1, min2, max2 } = props; return bool ? ((value - min1) * this.andDegree1) / (max1 - min1) : ((value - min2) * this.andDegree2) / (max2 - min2); } // 计算路径路径 createSvgPath(deltaDeg = 0, back = true) { const { r } = this.getCircleInfo(); const { startDegree1, startDegree2 } = this; const { scaleHeight1 } = this.props; const innerRadius = r - scaleHeight1; const countDegree = this.mapDeltaDegToScaleCount(deltaDeg, back); const endDegree = back ? (countDegree + startDegree1) % 360 : (startDegree2 - countDegree) % 360; const startAngle = back ? ((startDegree1 % 360) * Math.PI) / 180 : ((startDegree2 % 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 path = `M${_x1} ${_y1} A${innerRadius} ${innerRadius} 0 ${countDegree > 180 ? 1 : 0} ${ back ? 1 : 0 } ${_x2} ${_y2}`; return path; } render() { const responder = this.getResponder(); const { backColor, backStrokeOpacity, foreStrokeOpacity, foreColor, style, scaleHeight1, scaleHeight2, thumbFill, thumbStrokeWidth, thumbStroke, thumbFill2, thumbStrokeWidth2, thumbStroke2, thumbRadius1, thumbRadius2, needCircle1, startColor, needCircle2, endColor, } = this.props; const { r } = this.getCircleInfo(); const size = r * 2; const isGradient = foreColor && typeof foreColor === 'object'; const svgWidth = thumbRadius1 > thumbRadius2 ? size + 2 * thumbRadius1 : size + 2 * thumbRadius2; const view = thumbRadius1 > thumbRadius2 ? thumbRadius1 : thumbRadius2; return ( <View {...responder} style={[ style, { width: svgWidth, height: svgWidth, }, ]} > <Svg viewBox={`${-view} ${-view} ${svgWidth} ${svgWidth}`} width={svgWidth} height={svgWidth} > <Path d={this.backScalePath1} x="0" y="0" fill="none" stroke={backColor} strokeWidth={scaleHeight1} strokeOpacity={backStrokeOpacity} /> <ProgressCircle cx={this.startX1} cy={this.startY1} r={scaleHeight1 / 2 - 1} fill={startColor} stroke={startColor} /> <ProgressCircle cx={this.endX1} cy={this.endY1} r={scaleHeight1 / 2 - 1} fill={endColor} stroke={endColor} /> <Path d={this.backScalePath2} x="0" y="0" fill="none" stroke={backColor} strokeWidth={scaleHeight2} strokeOpacity={backStrokeOpacity} /> <ProgressCircle cx={this.startX2} cy={this.startY2} r={scaleHeight2 / 2 - 1} fill={startColor} stroke={startColor} /> <ProgressCircle cx={this.endX2} cy={this.endY2} r={scaleHeight2 / 2 - 1} fill={startColor} stroke={endColor} /> <PathCustom isGradient={isGradient} path={this.foreScalePath1} strokeOpacity={foreStrokeOpacity} strokeWidth={scaleHeight1} foreColor={foreColor} /> <PathCustom isGradient={isGradient} path={this.foreScalePath2} strokeOpacity={foreStrokeOpacity} strokeWidth={scaleHeight2} foreColor={foreColor} /> {needCircle1 && ( <ProgressCircle cx={this.progressX1} cy={this.progressY1} r={thumbRadius1} fill={thumbFill} strokeWidth={thumbStrokeWidth} stroke={thumbStroke} /> )} {needCircle2 && ( <ProgressCircle cx={this.progressX2} cy={this.progressY2} r={thumbRadius2} fill={thumbFill2} strokeWidth={thumbStrokeWidth2} stroke={thumbStroke2} /> )} </Svg> </View> ); } }