React Native 浅入门 —— 变形、动画篇

权当一个笔记,再写写或许更明白点

本不想这么快就写这个的,不过用到了,顺便记录一下。

变形(2D 或 3D 转换)

支持的东西在变化,请查看 /Libraries/StyleSheet/TransformPropTypes.js 确定当前支持的属性。

查看现有官方文档中 View 的 style 支持,会发现这么几个: rotation scaleX scaleY transformMatrix translateX translateY

但是很不幸滴,如果去看 TransformPropTypes.js 就会发现,这几个已经被标成了:DEPRECATED

甭这么用了,就算现在能用也是一样。

不过官方还给了这么一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
transform: ReactPropTypes.arrayOf(
ReactPropTypes.oneOfType([
ReactPropTypes.shape({perspective: ReactPropTypes.number}),
ReactPropTypes.shape({rotate: ReactPropTypes.string}),
ReactPropTypes.shape({rotateX: ReactPropTypes.string}),
ReactPropTypes.shape({rotateY: ReactPropTypes.string}),
ReactPropTypes.shape({rotateZ: ReactPropTypes.string}),
ReactPropTypes.shape({scale: ReactPropTypes.number}),
ReactPropTypes.shape({scaleX: ReactPropTypes.number}),
ReactPropTypes.shape({scaleY: ReactPropTypes.number}),
ReactPropTypes.shape({translateX: ReactPropTypes.number}),
ReactPropTypes.shape({translateY: ReactPropTypes.number})
])
)

所以实际上已经整体将变形、动画相关的切到了 transform 上。

来看一个简单的例子,看了再对比上面那段,就知道怎么用了:

1
2
3
4
5
6
7
var styles = StyleSheet.create({
line: {
transform: [
{rotate: '45deg'} // 旋转45度
]
}
});

这些属性都能在 css3 中找到,所以就分类看下好了。

perspective(0.9.0-stable开始支持)

这个说明来自于:W3school
perspective 属性定义 3D 元素距视图的距离,以像素计。该属性允许您改变 3D 元素查看 3D 元素的视图。
当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。
注意:perspective 属性只影响 3D 转换元素。

旋转

值类型为字符串,例如 ‘9deg’,表示9度。

  • rotate:2D旋转,绕着中心顺时针旋转多少度。
  • rotateX, rotateY, rotateZ:3D旋转,分别是绕着 X 轴、Y 轴和 Z 轴。(0.9.0-stable开始支持)

缩放

值类型为数字,表示倍数。

  • scale:X、Y 同时缩放
  • scaleX:仅 X 轴方向缩放
  • scaleY:仅 Y 轴方向缩放

移动

值行为数字:单位应该是坐标轴单位

  • translateX:X 轴方向移动
  • translateY:Y 周方向移动

动画

上面的都是设置了 style,达到一个变形的效果,那么能不能让它们动起来呢?

简单的 View,再设置 state 的做法是不行的,就是一个跳跃的效果。

现存的翻译过的文档都没有详细些的内容,这也是因为动画部分官方正在开发中,不过已经放出来了,而且E文的文档已经有更新,就翻译一些过来,顺便做点实验:

Animated 库被设计用来简便的实现风格多样的动画行为,同时又有较高的性能。

Animated 主要是通过声明初始/目标值,并且配置过渡的动画方式,然后通过简单的 start/stop 方法来控制动画的执行。

按官方说法要比设置 state 和 prerender 快很多滴。

支持元素

如例子中的 Animated.Image,默认支持:Image、Text和 View。

不过可以通过 Animated.createAnimatedComponent 方法来创造你想要的 Component。

动画效果(速度算法)

  • sprint:【弹性】简单的单弹簧物理模型,弹性的动画效果,效果符合 Origami
    • friction: 摩擦力…… 默认 7
    • tension: 张力 默认 40
  • decay: 【渐缓】以一个初始速度开始,并逐渐减慢直至停止
    • velocity: 初始速度,必须
    • deceleration: 减速率. 默认 0.997
  • timing: 指定时间长度的平缓动画
    • duration: 时间长度,单位 ms, 默认 500
    • easing: 定义曲线的平缓函数. 可查看 Easing module,其中已预定义了一些函数. iOS 默认是 Easing.inOut(Easing.ease).
    • delay: 延迟多少 ms 执行动画,默认 0

动画的执行(APIs)

执行 start 方法开始动画效果,start 可以接受一个 callback 参数,在动画完成时触发执行。
如果动画正常执行结束,callback 会接收到一个参数 {finished: true},如果是非正常结束,例如被手势或其他动画打断了,finished 则为 false。

一个 Component 展现时自动触发动画

主要是使用 componentDidMount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class SampleAnimation extends Component {
constructor(props: any) {
super(props);
this.state = {
bounceValue: new Animated.Value(0)
};
}
render(): ReactElement {
return (
<Animated.View // 支持: Image, Text, View
style={
{
flex: 1,
transform: [
{scale: this.state.bounceValue}
]
}
}
/>
);
}
componentDidMount() {
this.state.bounceValue.setValue(1.5); // 目标值
Animated.spring( // 支持: spring, decay, timing,过渡的动画方式
this.state.bounceValue,
{
toValue: 0.8, // 目标值
friction: 1 // 动画方式的参数
}
).start(); // 开始
}
}

动画的组合

可以使用以下方法来进行组合:

  • parallel 并行
  • sequence 顺序执行
  • stagger 交错??
  • delay 延迟执行

这四个方法都接受数组参数,元素内容是动画效果。

如果任何一个动画被 stoped,则其他动画也会终止。
Parallel有一个参数:stopTogether,如果被设置为 false,则不会执行这种终止的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Animated.sequence([ // spring to start and twirl after decay finishes
Animated.decay(position, { // coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
}),
Animated.parallel([ // after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0} // return to start
}),
Animated.timing(twirl, { // and twirl
toValue: 360,
}),
]),
]).start(); // start the sequence group

改变输入输出值域映射:interpolate

例如:[0, 1]映射[0, 100]为:

1
2
3
4
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

同样支持多重值域:

1
2
3
4
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});

这意味着:

-300 => 300
-200 150
-100 0
-50 0.5
0 1
50 0.5
100 0
101 0

这几个很容易理解,主要是边界值:
最左侧的[-300, -100]对应着[300, 0],这意味着,这种映射会一直向负数方向延伸下去,因此 -400 => 450
最右侧是[100, 101]对应这[0, 0],这意味着此后的数目都对应0,即 200 => 0

interpolate 支持动画计算函数,很多已经在 Libraries/Animation/Animated/Easing.js 中定义了。

interpolate可配置,默认是 extend,不过 clamp 在防止输出超出值域上同样十分有用。

toValue 可以接受另一个 Animated.Value 作为参数

Animated.events

输入事件,可以让手势或其他事件来直接对应动画的值,这当然需要一个结构化的数据才能支持:
第一层是数组,元素是对象:

scrollX 对应着 event.nativeEvent.contentOffset.x
pan.x and pan.y 对应着 gestureState.dx 和 gestureState.dy

1
2
3
4
5
6
7
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}] // scrollX = e.nativeEvent.contentOffset.x
)}
onPanResponderMove={Animated.event([
null, // ignore the native event
{dx: pan.x, dy: pan.y} // extract dx and dy from gestureState
]);

spring.stopAnimation(callback) 终止动画

spring.addListener(callback) 增加一个 listener,在动画执行时

LayoutAnimation

允许全局的进行动画的创建和更新,这会在下次的渲染时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var App = React.createClass({
componentWillMount() {
// Animate creation
LayoutAnimation.spring();
},
getInitialState() {
return { w: 100, h: 100 }
},
_onPress() {
// Animate the update
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15})
},
render: function() {
return (
<View style={styles.container}>
<View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
<TouchableOpacity onPress={this._onPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Press me!</Text>
</View>
</TouchableOpacity>
</View>
);
}
});

requestAnimationFrame

一般来说用不到这个 来自于 browser 的 API,Animations 基本封装好了。

不过,我还是来个简单的例子吧,一个超级简陋的时钟秒针的动画(这结合了绘图篇的知识):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Timing extends Component {
constructor(props) {
super(props);
this.state = {
secondRotation: (new Date()).getSeconds() * 6;
};
}
animate() {
var secondRotation = (new Date()).getSeconds() * 6;
this.setState({secondRotation});
requestAnimationFrame(this.animate.bind(this));
}
componentDidMount() {
requestAnimationFrame(this.animate.bind(this));
}
render() {
return (
<View>
<Surface
width={300}
height={400}
style={
{backgroundColor: 'blue'}
}>
<Group x={150} y={150}>
<Shape stroke="#000000" strokeWidth={4} d={line} rotation={this.state.secondRotation} />
</Group>
</Surface>
</View>
);
}
}

to be continued.