UNPKG

tuya-panel-kit

Version:

a functional component library for developing tuya device panels!

402 lines (377 loc) 10.2 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 Gradient from './gradient'; export default class ProgressSpace extends Gesture { static propTypes = { ...Gesture.propTypes, /** * 渐变ID */ gradientId: PropTypes.string, /** * 进度条样式 */ style: ViewPropTypes.style, /** * 具体值 */ value: PropTypes.number, /** * 开始角度 */ startDegree: PropTypes.number, /** * 在开始的角度上增加的角度 */ andDegree: PropTypes.number, /** * 最小值 */ min: PropTypes.number, /** * 最大值 */ max: PropTypes.number, /** * 步长 */ stepValue: PropTypes.number, /** * 大于具体值的不透明度 */ backStrokeOpacity: PropTypes.number, /** * 小于具体值的不透明度 */ foreStrokeOpacity: PropTypes.number, /** * 进度条渲染线条的数目 */ scaleNumber: PropTypes.number, /** * 进度条渲染的高度 */ scaleHeight: PropTypes.number, /** * 进度条是否可以手势滑动 */ disabled: PropTypes.bool, /** * 大于具体值的颜色 */ backColor: PropTypes.string, /** * 小于具体值的颜色 */ foreColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), /** * 值改变的回调 * @param {number} value - 具体值 */ onValueChange: PropTypes.func, /** * 滑动结束的回调 * @param {number} value - 具体值 */ onSlidingComplete: PropTypes.func, /** * 渐变起始点的x轴坐标 */ x1: PropTypes.string, /** * 渐变终点的x轴坐标 */ x2: PropTypes.string, /** * 渐变起始点的y轴坐标 */ y1: PropTypes.string, /** * 渐变终点的y轴坐标 */ y2: PropTypes.string, /** * 圆环中心自定义内容 */ renderCenterView: PropTypes.element, /** * 进度条块状的宽度 */ strokeWidth: PropTypes.number, }; static defaultProps = { ...Gesture.defaultProps, gradientId: 'Space', value: 50, startDegree: 135, andDegree: 270, min: 0, max: 100, stepValue: 0, scaleNumber: 120, scaleHeight: 9, disabled: false, backColor: '#E5E5E5', foreColor: '#FF4800', onValueChange() {}, onSlidingComplete() {}, style: null, backStrokeOpacity: 1, foreStrokeOpacity: 1, x1: '0%', y1: '0%', x2: '100%', y2: '0%', renderCenterView: null, strokeWidth: 0, }; constructor(props) { super(props); this.fixDegreeAndBindToInstance(props); this.state = { value: props.value, }; } componentWillReceiveProps(nextProps) { this.fixDegreeAndBindToInstance(nextProps); if (this.state.value !== nextProps.value) { this.setState({ value: nextProps.value, }); } } fixDegreeAndBindToInstance(props) { const { startDegree, andDegree } = props; this.startDegree = startDegree; this.endDegree = startDegree + andDegree; // 当初始度数大于360度时 if (startDegree >= 360) { this.startDegree = startDegree % 360; this.endDegree = (startDegree + andDegree) % 360; } // 基础圆环路径 this.backScalePath = this.createSvgPath(andDegree); // 具体值对应的角度 const deltaDeg = this.mapValueToDeltaDeg(props); // 小于具体值的路径 this.foreScalePath = this.createSvgPath(deltaDeg); } onStartShouldSetResponder({ nativeEvent: { locationX, locationY } }) { return this.shouldSetResponder(locationX, locationY); } shouldSetResponder(x0, y0) { const { scaleHeight, disabled } = this.props; if (disabled) { return false; } const { r } = this.getCircleInfo(); const { x, y } = this.getXYRelativeCenter(x0, y0); const len = Math.sqrt(x * x + y * y); const innerR = r - scaleHeight; const should = this.shouldUpdateScale(x0, y0); const finalShould = should && len <= r && len >= innerR; return finalShould; } shouldUpdateScale(x, y) { const { startDegree, endDegree } = this; const deg = this.getDegRelativeCenter(x, y); let should; if (endDegree < 360) { should = deg >= startDegree && deg <= endDegree; } else { should = deg >= startDegree || deg <= endDegree % 360; } 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 } = this; const deg = this.getDegRelativeCenter(locationX, locationY); const isInArea = this.shouldUpdateScale(locationX, locationY); if (isInArea) { let deltaDeg = deg - startDegree; if (deltaDeg < 0) { deltaDeg = deg + 360 - startDegree; } this.foreScalePath = this.createSvgPath(deltaDeg); const value = this.mapDeltaDegToValue(deltaDeg); if (typeof fn === 'function') fn(value); this.setState({ value, }); } if (isRelease && !isInArea) { const { value } = this.state; if (typeof fn === 'function') fn(value); } } 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) { const { scaleNumber, andDegree } = this.props; const eachDeg = andDegree / scaleNumber; let count = Math.ceil(deltaDeg / eachDeg); if (count > scaleNumber) count = scaleNumber; return count; } mapDeltaDegToValue(deltaDeg) { const count = this.mapDeltaDegToScaleCount(deltaDeg); const { min, max, scaleNumber, andDegree, stepValue } = this.props; if (stepValue) { const eachDeg = andDegree / scaleNumber; const deltaValue = max - min; const value = Math.round((count * eachDeg * deltaValue) / stepValue / andDegree); return Math.max(min, Math.min(max, value * stepValue + min)); } const eachDeg = andDegree / scaleNumber; const deltaValue = max - min; const value = (count * eachDeg * deltaValue) / andDegree; return Math.max(min, Math.min(max, value + min)); } // 具体值对应的角度 mapValueToDeltaDeg(props) { const { andDegree } = this.props; const { min, max, value } = props; return ((value - min) * andDegree) / (max - min); } // 计算路径路径 createSvgPath(deltaDeg = 0) { if (deltaDeg === 0) return ''; const { r } = this.getCircleInfo(); const { startDegree } = this; const { scaleNumber, scaleHeight, andDegree } = this.props; // 每个角度 const eachDeg = andDegree / scaleNumber; const innerRadius = r - scaleHeight; const count = this.mapDeltaDegToScaleCount(deltaDeg); let path = ''; for (let i = 0; i <= count; i++) { const pointDeg = startDegree + i * eachDeg; const pointAngle = (pointDeg * Math.PI) / 180; const _x1 = r + r * Math.cos(pointAngle); const _y1 = r + r * Math.sin(pointAngle); const _x2 = r + innerRadius * Math.cos(pointAngle); const _y2 = r + innerRadius * Math.sin(pointAngle); path += `M${_x1} ${_y1} L${_x2} ${_y2}`; } return path; } render() { const responder = this.getResponder(); const { backColor, backStrokeOpacity, foreStrokeOpacity, foreColor, style, strokeWidth, gradientId, x1, x2, y1, y2, renderCenterView, min, } = this.props; const { r } = this.getCircleInfo(); const size = r * 2; const isGradient = foreColor && typeof foreColor === 'object'; const greater = this.state.value !== min; return ( <View {...responder} style={[ { width: 125, height: 125, }, style, ]} > <Svg width={size} height={size}> <Path d={this.backScalePath} x="0" y="0" fill="none" stroke={backColor} strokeOpacity={backStrokeOpacity} strokeWidth={strokeWidth} /> {isGradient && greater && ( <Gradient gradientId={gradientId} x1={x1} x2={x2} y1={y1} y2={y2} isGradient={isGradient} foreColor={foreColor} /> )} <PathCustom isGradient={isGradient} path={this.foreScalePath} strokeOpacity={foreStrokeOpacity} strokeWidth={strokeWidth} gradientId={gradientId} foreColor={foreColor} /> </Svg> {renderCenterView} </View> ); } }