201808编辑:
先上两个自己写的库:
https://www.npmjs.com/package/react-native-animation-piechart
https://github.com/zramals/react-native-animation-pieChart
欢迎使用~
接下来原文:
一、ART库
目前我使用的RN版本是0.42,安卓自己就集成了,不需要额外的操作,iOS方面需要在podfile里面添加ART库
pod 'React', :path => '../rn-source', :subspecs => [
'Core',
'RCTActionSheet',
'RCTText',
'RCTImage',
'ART',
# needed for debugging
# Add any other subspecs you want to use in your project
]
二、画扇形
iOS使用arc函数是直接可以画的,但是安卓这个函数却不能很好的完成任务,需要一些特殊处理,使用另类的方法来完成。
这里也是改了网上的wedge,来完善安卓方面的绘制。
/**
* Copyright (c) 2013-present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule Wedge.art
* @typechecks
*
* Example usage:
* <Wedge
* outerRadius={50}
* startAngle={0}
* endAngle={360}
* fill="blue"
* />
*
* Additional optional property:
* (Int) innerRadius
*
*/
import React, {Component, PropTypes} from 'react';
import {Platform, ART} from 'react-native';
const {Shape, Path} = ART;
/**
* Wedge is a React component for drawing circles, wedges and arcs. Like other
* ReactART components, it must be used in a <Surface>.
*/
class Wedge extends Component {
constructor(props) {
super(props);
this.circleRadians = Math.PI * 2;
this.radiansPerDegree = Math.PI / 180;
this._degreesToRadians = this._degreesToRadians.bind(this);
}
/**
* degreesToRadians(degrees)
*
* Helper function to convert degrees to radians
*
* @param {number} degrees
* @return {number}
*/
_degreesToRadians(degrees) {
if (degrees !== 0 && degrees % 360 === 0) { // 360, 720, etc.
return this.circleRadians;
}
return degrees * this.radiansPerDegree % this.circleRadians;
}
/**
* createCirclePath(or, ir)
*
* Creates the ReactART Path for a complete circle.
*
* @param {number} or The outer radius of the circle
* @param {number} ir The inner radius, greater than zero for a ring
* @return {object}
*/
_createCirclePath(or, ir) {
const path = new Path();
path.move(0, or).arc(or * 2, 0, or).arc(-or * 2, 0, or);
if (ir) {
path.move(or - ir, 0).counterArc(ir * 2, 0, ir).counterArc(-ir * 2, 0, ir);
}
path.close();
return path;
}
/**
* _createArcPath(sa, ea, ca, or, ir)
*
* Creates the ReactART Path for an arc or wedge.
*
* @param {number} startAngle The starting degrees relative to 12 o'clock
* @param {number} endAngle The ending degrees relative to 12 o'clock
* @param {number} or The outer radius in pixels
* @param {number} ir The inner radius in pixels, greater than zero for an arc
* @return {object}
*/
_createArcPath(startAngle, endAngle, or, ir) {
const path = new Path();
// angles in radians
const sa = this._degreesToRadians(startAngle);
const ea = this._degreesToRadians(endAngle);
// central arc angle in radians
const ca = sa > ea
? this.circleRadians - sa + ea
: ea - sa;
// cached sine and cosine values
const ss = Math.sin(sa);
const es = Math.sin(ea);
const sc = Math.cos(sa);
const ec = Math.cos(ea);
// cached differences
const ds = es - ss;
const dc = ec - sc;
const dr = ir - or;
// if the angle is over pi radians (180 degrees)
// we will need to let the drawing method know.
const large = ca > Math.PI;
// TODO (sema) Please improve theses comments to make the math
// more understandable.
//
// Formula for a point on a circle at a specific angle with a center
// at (0, 0):
// x = radius * Math.sin(radians)
// y = radius * Math.cos(radians)
//
// For our starting point, we offset the formula using the outer
// radius because our origin is at (top, left).
// In typical web layout fashion, we are drawing in quadrant IV
// (a.k.a. Southeast) where x is positive and y is negative.
//
// The arguments for path.arc and path.counterArc used below are:
// (endX, endY, radiusX, radiusY, largeAngle)
// Update by Gene Xu to fix android issue, follow below
// https://github.com/facebook/react-native/blob/master/Libraries/ART/ARTSerializablePath.js
// https://github.com/bgryszko/react-native-circular-progress/blob/master/src/CircularProgress.js
// https://github.com/nihgwu/react-native-pie
const ARC = 4;
const CIRCLE_X = or;
const CIRCLE_Y = or;
const RX = or - or / 2;
const TwoPI = 2 * Math.PI;
if (Platform.OS === 'ios') {
path.move(or + or * ss, or - or * sc). // move to starting point
arc(or * ds, or * -dc, or, or, large). // outer arc
line(dr * es, dr * -ec); // width of arc or wedge
} else {
path.path.push(ARC, CIRCLE_X, CIRCLE_Y, RX, startAngle / 360 * TwoPI, (startAngle / 360 * TwoPI) - ((endAngle - startAngle) / 360 * TwoPI), 0)
}
if (ir) {
path.counterArc(ir * -ds, ir * dc, ir, ir, large); // inner arc
}
return path;
}
render() {
// angles are provided in degrees
const startAngle = this.props.startAngle;
const endAngle = this.props.endAngle;
if (startAngle - endAngle === 0) {
return;
}
// radii are provided in pixels
const innerRadius = this.props.innerRadius || 0;
const outerRadius = this.props.outerRadius;
// sorted radii
const ir = Math.min(innerRadius, outerRadius);
const or = Math.max(innerRadius, outerRadius);
let path;
if (endAngle >= startAngle + 360) {
path = this._createCirclePath(or, ir);
} else {
path = this._createArcPath(startAngle, endAngle, or, ir);
}
if (Platform.OS === 'ios') {
return <Shape {...this.props} d={path}/>;
} else {
return <Shape d={path} stroke={this.props.fill} strokeWidth={outerRadius} strokeCap='butt' />;
}
}
}
Wedge.propTypes = {
outerRadius: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
innerRadius: PropTypes.number
};
export default Wedge;
外部使用<wedge>进行画扇形的操作,对于内切圆,
_handleCover() {
const radius = this.props.outerRadius;
const coverRadius = this.props.innerRadius * 2;
const coverPath = new Path()
.moveTo(radius, this.props.outerRadius - this.props.innerRadius)
.arc(0, coverRadius, this.props.innerRadius)
.arc(0, -coverRadius, this.props.innerRadius)
.close();
return <Shape d={coverPath} fill={'white'} />;
}
使用该方法做覆盖操作,在显示上就完成了。
三、添加动画
还是直接上代码
import React, { Component, PropTypes } from 'react';
import { View, ART, Animated, Platform } from 'react-native';
import Wedge from './Wedge'
const { Surface, Shape, Path, Group } = ART;
var AnimatedWedge = Animated.createAnimatedComponent(Wedge);
export default class PieChat extends Component {
constructor(props) {
super(props);
this.wedgeAngles = [];
this.animationArray = [];
this.endAngleArray = [];
//初始化动画对象
for (var index = 0; index < this.props.percentArray.length; index++) {
this.animationArray.push(new Animated.Value(0));
}
this.state = {
animationArray: this.animationArray,
};
}
//保留同步执行的动画效果
// explode = () => {
// Animated.timing(this.state.animation1, {
// duration: 1500,
// toValue: 10
// }).start(() => {
// // this.state.animation.setValue(0);
// this.forceUpdate();
// });
// }
// explode2 = () => {
// Animated.timing(this.state.animation2, {
// duration: 1500,
// toValue: 10
// }).start(() => {
// // this.state.animation.setValue(0);
// this.forceUpdate();
// });
// }
_animations = () => {
var animatedArray = [];
for (var index = 0; index < this.props.percentArray.length; index++) {
animatedArray.push(Animated.timing(this.state.animationArray[index], {
duration: this.props.duration,
toValue: 10
}));
}
console.log('animation');
Animated.sequence(animatedArray).start();
}
_handleData = () => {
var wedgeAngles = [];
var percentArray = [];
var endAngleArray = [];
//处理百分比,得到每个部分的结束位置
for (var index = 0; index < this.props.percentArray.length; index++) {
var sum = 0;
for (var index2 = 0; index2 <= index; index2++) {
sum += this.props.percentArray[index2];
}
endAngleArray.push(sum);
}
this.endAngleArray = endAngleArray;
//添加动画对象数组
for (var index = 0; index < this.props.percentArray.length; index++) {
if (index === 0) {
wedgeAngles.push(this.state.animationArray[index].interpolate({
inputRange: [0, 10],
outputRange: [0.0001, this.endAngleArray[index] * 360],
extrapolate: 'clamp'
}));
} else if (index === this.props.percentArray.length - 1) {
wedgeAngles.push(this.state.animationArray[index].interpolate({
inputRange: [0, 10],
outputRange: [this.endAngleArray[index - 1] * 360 + 0.0001, 360],
extrapolate: 'clamp'
}));
}
else {
wedgeAngles.push(this.state.animationArray[index].interpolate({
inputRange: [0, 10],
outputRange: [this.endAngleArray[index - 1] * 360 + 0.0001, this.endAngleArray[index - 1] * 360 + this.props.percentArray[index] * 360],
extrapolate: 'clamp'
}));
}
}
this.wedgeAngles = wedgeAngles;
}
componentDidMount() {
this._handleData();
this._animations();
}
componentDidUpdate() {
this._handleData();
this._animations();
}
_handleCover() {
const radius = this.props.outerRadius;
const coverRadius = this.props.innerRadius * 2;
const coverPath = new Path()
.moveTo(radius, this.props.outerRadius - this.props.innerRadius)
.arc(0, coverRadius, this.props.innerRadius)
.arc(0, -coverRadius, this.props.innerRadius)
.close();
return <Shape d={coverPath} fill={'white'} />;
}
render() {
const rotation = Platform.OS === 'ios' ? 0 : -90;
console.log('render me');
return (
<Surface width={this.props.outerRadius * 2} height={this.props.outerRadius * 2}>
<Group rotation={rotation} originX={this.props.outerRadius} originY={this.props.outerRadius}>
{this.wedgeAngles.map((data, index) => {
if (index === 0) {
return <AnimatedWedge
key={index}
outerRadius={this.props.outerRadius}
startAngle={0}
endAngle={this.wedgeAngles[index]}
fill={this.props.colorArray[index]}
/>
} else {
return <AnimatedWedge
key={index}
outerRadius={this.props.outerRadius}
startAngle={this.endAngleArray[index - 1] * 360}
endAngle={this.wedgeAngles[index]}
fill={this.props.colorArray[index]}
/>
}
})}
{this._handleCover()}
</Group>
</Surface>
)
}
}
PieChat.propTypes = {
percentArray: React.PropTypes.array.isRequired,
colorArray: React.PropTypes.array.isRequired,
innerRadius: React.PropTypes.number,
outerRadius: React.PropTypes.number.isRequired,
duration: React.PropTypes.number,
};
PieChat.defaultProps = {
innerRadius: 0,
duration: 1500,
}
外部直接是用<pieChat>做使用
四、使用的注意事项
##############################
注意事项
##############################
使用的页面必须有state初始化,可以为空,用来触发动画的绘制。
若从外部传入参数,则建议使用
componentWillReceiveProps(nextProps) {
if (nextProps) {
this.setState({
percent: nextProps.percent,
})
setTimeout(() => {
this.setState({
percent: nextProps.percent,
})
}, 0);
}
}
来设定传入参数,触发动画。
##############################
#example
<PieChat
percentArray={[0.4, 0.6]}
colorArray={['#4d84eb', '#fca63e']}
outerRadius={40}
innerRadius={25}
duration={1000}
/>
##属性
PieChat.propTypes = {
percentArray: React.PropTypes.array.isRequired,
colorArray: React.PropTypes.array.isRequired,
innerRadius: React.PropTypes.number,
outerRadius: React.PropTypes.number.isRequired,
duration: React.PropTypes.number,
};
PieChat.defaultProps = {
innerRadius: 0,
duration: 1500,
}
### 属性定义
| 名称 | 类型 | 描述 |
|------|------|-------------|
| `percentArray` | `array` | 扇形各段的百分比,需自行计算,1为整个圆形图案 ,必填|
| `colorArray` | `array` | 扇形各段的颜色,按顺序,与percentArray数量须相同 ,必填|
| `innerRadius` | `number` | 扇形内环半径,默认为0(圆形),扇形效果需设置,需小于外环半径|
| `outerRadius` | `number` | 扇形外环半径,必填|
| `duration` | `number` | 每段动画时长,默认1500毫秒 |