弁言
一直以来,动画都是挪动开辟中极为特别的一块。一方面,动画在交互体验上有着不可替换的优胜处,然则另一方面,动画的开辟又极为的耗时,须要斲丧工程师大批的时候用于开辟和调试。再来看前端,前端的动画完成,经由多年的生长,已分为 CSS3 动画和 JavaScript 动画。
React Native 作为一个复用前端头脑的挪动开辟框架,并没有完全完成CSS,而是运用JavaScript来给运用增加款式。这是一个有争议的决议,可以参考这个幻灯片来相识 Facebook 做的来由。天然,在动画上,由于缺少大批的 CSS 属性,React Naive 中的动画均为 JavaScript 动画,即经由历程 JavaScript 代码掌握图象的种种参数值的变化,从而发生时候轴上的动画结果。
React Native 的官方文档已细致地引见了 React Native 平常动画的运用要领和实例,在此不再赘述。然则浏览官方文档后可知,官方的动画往往是给一个完全的物体增加种种动画结果,如透明度,翻转,挪动等等。然则关于物体的本身变化,比方以下这个进度条,显著是在扭转的同时也在伸缩,则缺少必要的完成要领。这是由于,动画的实质既是图形的种种参数的数值变化的历程,文档中的 Animated.Value 就是用作被驱动的参数,可以,想要让一个圆环可以伸缩,就必需让数值变化的历程,深切到图形天生的历程当中,而不是如官方文档的例子一样,仅仅是施加于图形天生终了后的历程,那末也就没法完成转变图形本身的动画结果了。
拙作初窥基于 react-art 库的 React Native SVG已议论了 React Native 中静态 SVG 的开辟要领,本文则致力于探讨 React Native 中 SVG 与 Animation 连系所完成的 SVG 动画。也就是可以转变图形本身的动画结果。别的还探讨了 Value 驱动动画在完成要领上的不同之处。
Props 驱动的 SVG 动画
本节即以完成一个下图所示的扭转的进度条的例子,报告 React Native SVG 动画的开辟要领。
Wedge.art.js 位于 react-art 库下 lib/ 文件夹内,供应了 SVG 扇形的完成,然则缺少对 cx, cy 属性的支撑。别的拙作之前也提到了,Wedge中的扇形较为诡异,只要一条半径,为了完成进度条结果我把另一条半径也去掉了。我将 Wedge.art.js 拷贝到工程中,自行小修正后的代码以下。
// wedge.js
/**
* Copyright 2013-2014 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
*
*/
'use strict';
var React = require('react-native');
var ReactART = React.ART;
var $__0 = React,PropTypes = $__0.PropTypes;
var Shape = ReactART.Shape;
var Path = ReactART.Path;
/**
* Wedge is a React component for drawing circles, wedges and arcs. Like other
* ReactART components, it must be used in a <Surface>.
*/
var Wedge = React.createClass({displayName: "Wedge",
propTypes: {
outerRadius: PropTypes.number.isRequired,
startAngle: PropTypes.number.isRequired,
endAngle: PropTypes.number.isRequired,
innerRadius: PropTypes.number,
cx: PropTypes.number,
cy: PropTypes.number
},
circleRadians: Math.PI * 2,
radiansPerDegree: Math.PI / 180,
/**
* _degreesToRadians(degrees)
*
* Helper function to convert degrees to radians
*
* @param {number} degrees
* @return {number}
*/
_degreesToRadians: function(degrees) {
if (degrees !== 0 && degrees % 360 === 0) { // 360, 720, etc.
return this.circleRadians;
} else {
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: function(or, ir) {
var path = Path();
path.move(this.props.cx, or + this.props.cy)
.arc(or * 2, 0, or)
.arc(-or * 2, 0, or);
if (ir) {
path.move(this.props.cx + or - ir, this.props.cy)
.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: function(startAngle, endAngle, or, ir) {
var path = Path();
// angles in radians
var sa = this._degreesToRadians(startAngle);
var ea = this._degreesToRadians(endAngle);
// central arc angle in radians
var ca = sa > ea ? this.circleRadians - sa + ea : ea - sa;
// cached sine and cosine values
var ss = Math.sin(sa);
var es = Math.sin(ea);
var sc = Math.cos(sa);
var ec = Math.cos(ea);
// cached differences
var ds = es - ss;
var dc = ec - sc;
var dr = ir - or;
// if the angle is over pi radians (180 degrees)
// we will need to let the drawing method know.
var 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)
path.move(or + or * ss + this.props.cx, or - or * sc + this.props.cy) // move to starting point
.arc(or * ds, or * -dc, or, or, large) // outer arc
// .line(dr * es, dr * -ec); // width of arc or wedge
if (ir) {
path.counterArc(ir * -ds, ir * dc, ir, ir, large); // inner arc
}
return path;
},
render: function() {
// angles are provided in degrees
var startAngle = this.props.startAngle;
var endAngle = this.props.endAngle;
if (startAngle - endAngle === 0) {
return;
}
// radii are provided in pixels
var innerRadius = this.props.innerRadius || 0;
var outerRadius = this.props.outerRadius;
// sorted radii
var ir = Math.min(innerRadius, outerRadius);
var or = Math.max(innerRadius, outerRadius);
var path;
if (endAngle >= startAngle + 360) {
path = this._createCirclePath(or, ir);
} else {
path = this._createArcPath(startAngle, endAngle, or, ir);
}
return React.createElement(Shape, React.__spread({}, this.props, {d: path}));
}
});
module.exports = Wedge;
然后就是完成的主体。个中值得关注的点是:
并不是任何 Component 都可以直接用
Animated.Value
去赋值 Props,而须要对 Component 做肯定的革新。Animated.createAnimatedComponent(Component component)
,是 Animated 库供应的用于把一般 Component 革新为 AnimatedComponent 的函数。浏览 React Native 源代码会发明,Animated.Text, Animated.View, Animated.Image,都是直接挪用了该函数去革新体系已有的组件,如Animated.createAnimatedComponent(React.Text)
。Easing 库较为隐藏,明显在
react-native/Library/Animated/
途径下,却又须要从React中直接引出。它为动画的完成供应了很多缓动函数,可根据现实需求挑选。如linear()
线性,quad()
二次(quad明显是四次方的意义,为毛代码完成是t*t….),cubic()
三次等等。官方文档中揄扬 Easing 中供应了 tons of functions(成吨的函数),然则我数过了明显才14个,233333。该动画由肇端角度和停止角度两个变化的参数来掌握,因而,两个
Animated.Value
须要同时启动,这触及到了动画的组合题目。React Native 为此供应了parallel
,sequence
,stagger
和delay
四个函数。其主要完成均可在react-native/Library/Animated/Animate中找到,官方文档中亦有申明。这里用的是Animated.parallel
。
开辟中碰到的题目有:
该动画在 Android 上可以运转,然则革新频次看上去只要两帧,没法构成一个天然过渡的动画,笔者怀疑是 React Native Android 对 SVG 的支撑仍有缺点。
SVG 图形和一般 React Native View 的叠加题目,现在我还没有找到解决要领。以为只能等 React Native 开辟组的进一步支撑。
动画播放总会有一个稀里糊涂的下拉回弹结果,然则代码上没有任何分外的掌握。
// RotatingWedge.js
'use strict';
var React = require('react-native');
var {
ART,
View,
Animated,
Easing,
} = React;
var Group = ART.Group;
var Surface = ART.Surface;
var Wedge = require('./Wedge');
var AnimatedWedge = Animated.createAnimatedComponent(Wedge);
var VectorWidget = React.createClass({
getInitialState: function() {
return {
startAngle: new Animated.Value(90),
endAngle: new Animated.Value(100),
};
},
componentDidMount: function() {
Animated.parallel([
Animated.timing(
this.state.endAngle,
{
toValue: 405,
duration: 700,
easing: Easing.linear,
}
),
Animated.timing(
this.state.startAngle,
{
toValue: 135,
duration: 700,
easing: Easing.linear,
})
]).start();
},
render: function() {
return (
<View>
<Surface
width={700}
height={700}
>
{this.renderGraphic()}
</Surface>
</View>
);
},
renderGraphic: function() {
console.log(this.state.endAngle.__getValue());
return (
<Group>
<AnimatedWedge
cx={100}
cy={100}
outerRadius={50}
stroke="black"
strokeWidth={2.5}
startAngle={this.state.startAngle}
endAngle={this.state.endAngle}
fill="FFFFFF"/>
</Group>
);
}
});
module.exports = VectorWidget;
Value 驱动的动画
接下来看 Value 驱动的 SVG 动画。先解释一下 Value 和 Props 的区分。<Text color='black'></Text>
,这里的 color 就是 Props,<Text>black</Text>
这里的 black 就是 value。
为何要特地强调这一点呢,假如我们想要做一个以下图所示的从10到30更改的数字,根据上节所述的要领,直接挪用 Animated.createAnimatedComponent(React.Text)
所天生的 Component ,然后给 Value 赋值一个Animated.Value(),然后Animated.timing…,是没法发生如许的结果的。
必需要对库中的createAnimatedComponent()
函数做肯定的革新。革新后的函数以下:
var AnimatedProps = Animated.__PropsOnlyForTests;
function createAnimatedTextComponent() {
var refName = 'node';
class AnimatedComponent extends React.Component {
_propsAnimated: AnimatedProps;
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
}
setNativeProps(props) {
this.refs[refName].setNativeProps(props);
}
componentWillMount() {
this.attachProps(this.props);
}
attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
/** 症结修正,强迫革新。
本来的代码是:
var callback = () => {
if (this.refs[refName].setNativeProps) {
var value = this._propsAnimated.__getAnimatedValue();
this.refs[refName].setNativeProps(value);
} else {
this.forceUpdate();
}
};
**/
var callback = () => {
this.forceUpdate();
};
this._propsAnimated = new AnimatedProps(
nextProps,
callback,
);
oldPropsAnimated && oldPropsAnimated.__detach();
}
componentWillReceiveProps(nextProps) {
this.attachProps(nextProps);
}
render() {
var tmpText = this._propsAnimated.__getAnimatedValue().text;
return (
<Text
{...this._propsAnimated.__getValue()}
ref={refName}
>
{Math.floor(tmpText)}
</Text>
);
}
}
return AnimatedComponent;
}
为了猎取必需要用到的AnimatedProps,笔者以至违犯了品德的束缚,访问了双下划线前缀的变量Animated.__PropsOnlyForTests
,真是罪行啊XD。
言归正传,主要的修正有:
修正了 attachProps 函数。关于任何更改的 props,本来的代码会试图运用 setNativeProps 函数举行更新,若 setNativeProps 函数为空,才会运用 forceUpdate() 函数。关于 props,setNativeProps 函数是可行的,然则对 value 无效。我猜想,setNativeProps 要领在 Android 底层能够就是 setColor() 相似的 Java 要领,然则并没有获得实证。现在这类 forceUpdate,由解释知,是完全更新了悉数 Component,相当于先从 DOM 树上取下一个旧节点,再放上一个新节点,在机能的利用上较为糟蹋。
运用 PropTypes.xxx.isRequired 来举行参数的范例搜检。PropTypes 搜检支撑的范例可在
react-native/node_modules/react/lib/ReactPropTypes.js
中看到,在此不再赘述。Animated.value() 从10到30变化的历程是一个随机采样的历程,并不肯定会卡在整数值上,因而还须要做一些小处置惩罚。
值得注意的是,该动画在 Android 上虽然可以一般运转,但也存在丢帧的题目,远远不能如 iOS 上流通天然。关于这一点,只能守候 Facebook 的进一步优化。
悉数的代码以下:
// RisingNumber.js
'use strict';
var React = require('react-native');
var {
Text,
Animated,
Easing,
PropTypes,
View,
StyleSheet,
} = React;
var AnimatedText = createAnimatedTextComponent();
var AnimatedProps = Animated.__PropsOnlyForTests;
function createAnimatedTextComponent() {
var refName = 'node';
class AnimatedComponent extends React.Component {
_propsAnimated: AnimatedProps;
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
}
setNativeProps(props) {
this.refs[refName].setNativeProps(props);
}
componentWillMount() {
this.attachProps(this.props);
}
attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
var callback = () => {
this.forceUpdate();
};
this._propsAnimated = new AnimatedProps(
nextProps,
callback,
);
oldPropsAnimated && oldPropsAnimated.__detach();
}
componentWillReceiveProps(nextProps) {
this.attachProps(nextProps);
}
render() {
var tmpText = this._propsAnimated.__getAnimatedValue().text;
return (
<Text
{...this._propsAnimated.__getValue()}
ref={refName}
>
{Math.floor(tmpText)}
</Text>
);
}
}
return AnimatedComponent;
}
var RisingNumber = React.createClass({
propTypes: {
startNumber: PropTypes.number.isRequired,
toNumber: PropTypes.number.isRequired,
startFontSize: PropTypes.number.isRequired,
toFontSize: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
upperText: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
number: new Animated.Value(this.props.startNumber),
fontSize: new Animated.Value(this.props.startFontSize),
};
},
componentDidMount: function() {
Animated.parallel([
Animated.timing(
this.state.number,
{
toValue: this.props.toNumber,
duration: this.props.duration,
easing: Easing.linear,
},
),
Animated.timing(
this.state.fontSize,
{
toValue: this.props.toFontSize,
duration: this.props.duration,
easing: Easing.linear,
}
)
]).start();
},
render: function() {
return (
<View>
<Text style={styles.kind}>{this.props.upperText}</Text>
<AnimatedText
style={{fontSize: this.state.fontSize, marginLeft: 15}}
text={this.state.number} />
</View>
);
},
});
var styles = StyleSheet.create({
kind: {
fontSize: 15,
color: '#01A971',
},
number: {
marginLeft: 15,
},
});
module.exports = RisingNumber;
====================================
假如您以为我的文章对您有所启示,请点击文末的引荐按钮,您的勉励将会成为我对峙写作的莫大鼓励。 by DesGemini