一、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毫秒 |