翻译自 React-native Animated API Basic Example
翻译过程中有删改
简介
本文是探索 react-native
中实现的的 Animated API
,Web 版本上的 React 没有该 API,不过可以使用在 react-europe 大会上发布的 react-motion。
本文中将会完成一个动画例子,效果如下图
原理
Animated API
的原理并非通过 setState
方法使 react 重渲染,而是使用 setNativeProps
方法更新 native 视图。Animated API
导出了几个特殊的 components:Animated.View
, Animated.Text
, 和 Animated.Image
。Animated API 直接在 Objective-C 的 native 环境中调整这些 components 的外观样式,跳过了 JS 环境中 react 的 diff 与 reconciliation 过程,从而获得流畅、高效的动画。
简而言之,它将对动画中变化的属性数值做插值运算并且刷新 native 视图。
动画效果:沿屏幕移动的方块
我们将实现一个简单的动画效果:沿手机屏幕四个边,按照左上角 -> 左下角 -> 右下角 -> 右上角的顺序,移动一个正方形。示意图大概如下
< -- <
| |
V -- ^
开始
导入依赖
import React, {
AppRegistry,
Component,
Dimensions,
StyleSheet,
View,
Animated
} from 'react-native';
const { width, height } = Dimensions.get('window');
const SQUARE_DIMENSIONS = 30;
样式
const styles = StyleSheet.create({
container: {
flex: 1
},
square: {
width: SQUARE_DIMENSIONS,
height: SQUARE_DIMENSIONS,
backgroundColor: 'blue'
}
});
基本逻辑
class AnimatedSquare extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
}
}
getStyle() {
return [styles.square, {
transform: this.state.pan.getTranslateTransform()
}];
}
render() {
return (
<View style={styles.container}>
<Animated.View style={this.getStyle()} />
</View>
);
}
}
上面代码中有几个需要解释的地方。
注意我们所建立的 component
的 state
是 Animated.ValueXY
的一个实例。这个 API 将在 X
、Y
两个值上进行插值。
getStyle()
方法,返回一个样式对象数组。包括描述了方块宽高大小的 square
基本样式,以及最为重要的,一个 transform
样式对象。
我们使用 getTranslateTransform
这个 Animated API 中的 helper 方法,来返回一个适合 transform
属性结构的值。
这个返回值的结构类似于[{ translateX: xValue}, {translateY: yValue}]
,xValue 和 yValue 是计算后的插值。
最后我们使用 Animated.View
,表示这个组件是可动画组件。
移动正方形
一开始正方形是静止在左上角的,现在我们把它从左上角(x = 0, y = 0
)移动到左下角(x = 0, y = (屏幕高 - 正方形高)
)
const SPRING_CONFIG = {tension: 2, friction: 3}; //Soft spring
//...
componentDidMount() {
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: height - SQUARE_DIMENSIONS} // return to start
}).start();
}
在组件装载后,我们通过 Animated.spring
进行 Spring(弹性)动画 ,我们给弹性动画设置了 SPRING_CONFIG
配置,包括 tension(张力)和 friction(摩擦)值,所以正方形到达左下角后,会有一个小小回弹动画。
再动,又动,还动
我们会建立一个顺序的动画序列,让动画一个接一个进行。当然除了 sequence(顺序),你还可以按 parallel(并行)组合动画效果,让动画同时进行。
componentDidMount() {
Animated.sequence([
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: height - SQUARE_DIMENSIONS} //animate to bottom left
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: height - SQUARE_DIMENSIONS} // animated to bottom right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: 0} //animate to top right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: 0} // return to start
})
]).start();
}
如之前设想的一样,我们定义了4个弹性动画。注释解释了动画移动方向。
一直不停动
Animated.sequence(animtionList: Arrary).start(cb: Function);
动画序列的start
方法可以传一个回调函数,在动画全部执行完时触发。在我们的例子中,这时候正方形回到了起点,我们可以重新开始一遍动画。
componentDidMount() {
this.startAndRepeat();
}
startAndRepeat() {
this.triggerAnimation(this.startAndRepeat);
}
triggerAnimation(cb) {
Animated.sequence([
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: height - SQUARE_DIMENSIONS} //animate to bottom left
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: height - SQUARE_DIMENSIONS} // animated to bottom right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: 0} //animate to top right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: 0} // return to start
})
]).start(cb);
}
我们把动画逻辑提取为一个方法,在完成回调函数中触发它。
全部代码
import React, {
AppRegistry,
Component,
Dimensions,
StyleSheet,
View,
Animated
} from 'react-native';
const { width, height } = Dimensions.get('window');
const SQUARE_DIMENSIONS = 30;
const SPRING_CONFIG = {tension: 2, friction: 3};
class AnimatedSquare extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
}
}
componentDidMount() {
this.startAndRepeat();
}
startAndRepeat() {
this.triggerAnimation(this.startAndRepeat);
}
triggerAnimation(cb) {
Animated.sequence([
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: height - SQUARE_DIMENSIONS} //animate to bottom left
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: height - SQUARE_DIMENSIONS} // animated to bottom right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: width - SQUARE_DIMENSIONS, y: 0} //animate to top right
}),
Animated.spring(this.state.pan, {
...SPRING_CONFIG,
toValue: {x: 0, y: 0} // return to start
})
]).start(cb);
}
getStyle() {
return [styles.square, {
transform: this.state.pan.getTranslateTransform()
}];
}
render() {
return (
<View style={styles.container}>
<Animated.View style={this.getStyle()} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
square: {
width: SQUARE_DIMENSIONS,
height: SQUARE_DIMENSIONS,
backgroundColor: 'blue'
}
});
AppRegistry.registerComponent('AnimatedSquare', () => AnimatedSquare);
其它一些范例
react-native-animated-demo-tinder
UIExplorer Animated example
相关资源
Cheng Lou – The State of Animation in React at react-europe 2015
react-motion – Github
React Native Animation API
Spencer Ahrens – React Native: Building Fluid User Experiences at react-europe 2015