RN使用ART库画扇形+动画

本文详细介绍如何在React Native中实现动态饼图,包括自定义组件、扇形绘制技巧及跨平台兼容性处理,并提供了完整的动画实现代码。

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

 

 

 

 

 

 

 

 

 

 

 

<think> 我们正在处理用户意图:了解公式 $Rn=G+H+LE$ 的物理意义。 根据之前的对话历史,用户已经了解FC=H+LE(即湍流通量)的物理意义,现在进一步询问地表能量平衡方程的完整形式。 搜索词建议: "地表能量平衡方程 Rn=G+H+LE 物理意义" "感热通量 潜热通量 土壤热通量 解释" "地表能量分配原理" 由于我们无法实际进行网络搜索,我们将基于已知的科学知识构建答案。在实际应用中,我们会调用搜索工具获取相关资料。 回答结构: 1. 方程的定义和总体物理意义(能量守恒) 2. 分解每个分量的物理意义(Rn, G, H, LE) 3. 实际应用(如气象、农业、气候研究) 4. 测量方法(简要提及) 注意:避免步骤词汇,将内容组织为多个段落,每个部分之间换行。 最后,生成3个相关问题。 我们将按照要求生成答案。 </think> ### 公式 $Rn = G + H + LE$ 的物理意义 该公式描述**地表能量平衡原理**,反映到达地表的净辐射能($Rn$)在不同能量传输途径中的分配机制: **$Rn$(净辐射)** - 地表接收的太阳辐射与反射/发射辐射的净值: $$Rn = S_{\downarrow} - S_{\uparrow} + L_{\downarrow} - L_{\uparrow}$$ $S$为短波辐射,$L$为长波辐射,箭头表示方向 - 驱动地表所有能量交换过程的总能源 **$G$(土壤热通量)** - 通过热传导进入土层存储的能量 - 日变化显著:正午吸热($G>0$),夜间释热($G<0$) - 计算公式: ```python # 土壤热通量板测量原理 G = λ * (dT/dz) # λ:土壤导热系数, dT/dz:垂直温度梯度 ``` - 占$Rn$比例:农田约5-15%,裸地可达30% **$H$(感热通量)** - 湍流形式传递的显热能,直接影响气温 - 物理机制: $$H = \rho c_p \overline{w'T'}$$ $\rho$:空气密度, $c_p$:定压比热, $\overline{w'T'}$:垂直风速-温度的协方差 - 主导干燥地表(如沙漠$H/Rn > 60\%$),形成热岛效应 **$LE$(潜热通量)** - 水相变(蒸发/蒸腾)消耗的能源 - 计算表达式: $$LE = L_v \cdot E$$ $L_v$:汽化潜热(2.47MJ/kg), $E$:蒸发速率 - 在湿润区占比显著(雨林$LE/Rn \approx 80\%$),调节区域湿度 --- ### 能量分配特征(典型地表) | 地表类型 | $G/Rn$ | $H/Rn$ | $LE/Rn$ | 能量去向 | |------------|--------|--------|---------|----------------------| | 城市 | 3-7% | 50-70% | 10-30% | 建筑加热/空调耗能 | | 森林 | 2-5% | 20-30% | 65-85% | 蒸腾作用为主 | | 农田灌溉 | 5-10% | 15-25% | 60-75% | 作物蒸腾耗水 | | 雪地 | 1-3% | 60-80% | 10-20% | 雪面升华/反射增强 | --- ### 应用价值 - **气象预报**:通过$H/LE$比例改进边界层参数化方案 - **农业管理**:监测$LE$优化灌溉($LE$突降预示缺水胁迫) - **气候变化**:极地$G$增加反映冻土退化,热带$LE$上升加剧水循环 - **测量技术**: ```python # 通量观测系统组成 components = { "净辐射计": "测量Rn", "土壤热流板": "测量G", "涡度协方差系统": "同步获取H和LE" } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值