RN使用ART库画扇形+动画

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

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值