tuya-panel-kit
Version:
a functional component library for developing tuya device panels!
552 lines (526 loc) • 14.9 kB
JavaScript
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 Progress 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,
/**
* 进度条渲染的高度
*/
scaleHeight: PropTypes.number,
/**
* 进度条是否可以手势滑动
*/
disabled: PropTypes.bool,
/**
* 大于具体值的颜色
*/
backColor: PropTypes.string,
/**
* 小于具体值的颜色
*/
foreColor: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.arrayOf(
PropTypes.shape({
offset: PropTypes.string.isRequired,
stopColor: PropTypes.string.isRequired,
stopOpacity: PropTypes.string.isRequired,
})
),
]),
/**
* 值改变的回调
* @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,
/**
* thumb小圆球的填充色
*/
thumbFill: PropTypes.string,
/**
* thumb小圆球边框宽度
*/
thumbStrokeWidth: PropTypes.number,
/**
* thumb小圆球的边框色
*/
thumbStroke: PropTypes.string,
/**
* thumb小圆球的半径
*/
thumbRadius: PropTypes.number,
/**
* 是否需要最大值的thumb
*/
needMaxCircle: PropTypes.bool,
/**
* 是否需要最小值的thumb
*/
needMinCircle: PropTypes.bool,
/**
* 轨道不满360度开始的圆环颜色
*/
startColor: PropTypes.string,
/**
* 轨道不满360度开始的圆环颜色
*/
endColor: PropTypes.string,
/**
* 圆环中心自定义内容
*/
renderCenterView: PropTypes.element,
};
static defaultProps = {
...Gesture.defaultProps,
gradientId: 'Progress',
value: 50,
startDegree: 135,
andDegree: 270,
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: '#fff',
thumbStrokeWidth: 2,
thumbRadius: 2,
needMaxCircle: true,
needMinCircle: false,
startColor: null,
endColor: null,
renderCenterView: null,
};
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 % 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(props);
// 小于具体值的路径
this.foreScalePath = this.createSvgPath(deltaDeg);
const { progressStartX, progressStartY, progressX, progressY } = this.getCirclePosition(
this.foreScalePath
);
this.startProgressX = progressStartX;
this.startProgressY = progressStartY;
this.progressX = progressX;
this.progressY = progressY;
}
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, y0);
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 (startDegree < endDegree) {
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 { needMaxCircle } = this.props;
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;
}
const value = this.mapDeltaDegToValue(deltaDeg);
this.foreScalePath = this.createSvgPath(deltaDeg);
const { progressX, progressY } = this.getCirclePosition(this.foreScalePath);
if (needMaxCircle) {
this.progressX = progressX;
this.progressY = progressY;
}
this.setState({
value,
});
if (typeof fn === 'function') fn(value);
}
if (isRelease && !isInArea) {
const { value } = this.state;
if (typeof fn === 'function') fn(value);
}
}
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 { thumbRadius } = this.props;
const { x: _x, y: _y } = this.getXYRelativeCenter(x - thumbRadius, y - thumbRadius);
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;
}
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(props) {
const { min, max, value } = props;
return ((value - min) * this.andDegree) / (max - min);
}
// 计算路径路径
createSvgPath(deltaDeg = 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 % 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;
if (countDegree === 360) {
const middleDegree = (this.mapDeltaDegToScaleCount(startDegree + 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,
needMaxCircle,
needMinCircle,
startColor,
endColor,
renderCenterView,
min,
max,
} = this.props;
const { value } = this.state;
const { r } = this.getCircleInfo();
const size = r * 2;
const isGradient = foreColor && typeof foreColor === 'object';
const greater = this.state.value !== min;
const minCircleColor =
value === min ? backColor : isGradient ? Object.values(foreColor)[0] : foreColor;
const maxCircleColor =
value === max
? isGradient
? Object.values(foreColor)[Object.values(foreColor).length - 1]
: foreColor
: backColor;
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 || minCircleColor}
stroke={startColor || minCircleColor}
/>
)}
{this.andDegree < 360 && (
<ProgressCircle
cx={this.endX}
cy={this.endY}
r={scaleHeight / 2 - 1}
fill={endColor || maxCircleColor}
stroke={endColor || maxCircleColor}
/>
)}
{isGradient && greater && (
<Gradient
gradientId={gradientId}
x1={x1}
x2={x2}
y1={y1}
y2={y2}
foreColor={foreColor}
/>
)}
<PathCustom
isGradient={isGradient}
path={this.foreScalePath}
strokeOpacity={foreStrokeOpacity}
strokeWidth={scaleHeight}
gradientId={gradientId}
foreColor={foreColor}
/>
{needMaxCircle && (
<ProgressCircle
cx={this.progressX}
cy={this.progressY}
r={thumbRadius}
fill={thumbFill}
strokeWidth={thumbStrokeWidth}
stroke={thumbStroke}
/>
)}
{needMinCircle && (
<ProgressCircle
cx={this.startProgressX}
cy={this.startProgressY}
r={thumbRadius}
fill={thumbFill}
strokeWidth={thumbStrokeWidth}
stroke={thumbStroke}
/>
)}
</Svg>
{renderCenterView}
</View>
);
}
}