RN提供了两套动画系统

  • Animated API
  • LayoutAnimation

他们分别有不同的应用场景。本文DEMO可以从QDAnimatedDemo 获取

基础动画

Animated API 主要是用来将视图的属性值映射成动画值,然后通过控制该动画值的变化关系来赋予视图定制化的动画效果,比如正方块从顶部往下掉,

变化值是正方块的top值,范围为[0, 300],首先定义一个动画值,初始值为0

1
topValue = new Animated.Value(0)//初始值为0

为topValue定义动画关系,使用spring动画(弹簧效果),终止值是300,即0~300

1
2
3
topAni = Animated.spring(topValue, {
toValue: 300
})

将topValue赋值于View的top属性,并开启动画。Animated Api对应的视图需要加上Animated前缀,如Animated.View,Animated.ScrollView

1
2
3
4
<Animated.View style={{top: topValue}}/>

//触发动画
topAni.start()

完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const SpringView = () => {    
const topValue = useRef(new Animated.Value(0)).current

useEffect(() => {
Animated.spring(topValue, {
toValue: 300
}).start()
}, [])

return (
</View>
<Animated.View style={[styles.block, {top: topValue}]}/>
</View>
)
}

将动画放在了useEffect中,当视图渲染完成后即刻开始执行动画,也可以绑定在事件上。

  • spring动画还可以设置其他参数,比如speed可以控制速度参数,bounciness则用来控制弹性系数等。
  • timing动画接受一个easing的函数,也就是控制缓入缓出,当插入贝塞尔曲线函数时可以实现物体的抛物线运动。
  • decay动画提供一个初速度,跟衰减系数,模仿物理运动,初速越快,衰减系数越小,则运动越远。

组合动画

多个不同的动画可以串行执行,也可以并行执行

  • 并行动画:使用Animated.parallel将需要并行的动画包裹在当中
1
2
3
4
5
6
7
8
9
10
11
Animated.parallel([            
Animated.timing(topAnim, {
toValue: 400,
useNativeDriver: false
}),

Animated.timing(sizeAnim, {
toValue: {x: 200, y: 200},
useNativeDriver: false
})
]).start()

  • 串行动画:使用Animated.sequence将需要串行的动画包裹在当中
1
2
3
4
5
6
7
8
9
10
11
Animated.sequence([            
Animated.timing(yAnim, {
toValue: 400,
useNativeDriver: false
}),

Animated.timing(sizeAnim, {
toValue: {x: 200, y: 200},
useNativeDriver: false
})
]).start()

插值

插值函数能将动画的输入跟输出值重新映射,生成一个新的动画。

假设这样一个场景,方块从顶部掉落的同时,让方块的透明值从1降到0.5的半透明状态,这既可以通过上述的并行动画来实现,也可以通过插值动画去映射。

方块的top值变动范围是[0, 300],opacity的变动范围则是[1, 0.5]

在第一个方块下落的例子中加入插值动画映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const SpringView = () => {    
const topValue = useRef(new Animated.Value(0)).current

useEffect(() => {
Animated.spring(topValue, {
toValue: 300
}).start()
}, [])

return (
</View>
<Animated.View style={[styles.block, {top: topValue},
{
{/* 使用插值映射透明度动画 */}
opacity: topValue.interpolate({
inputRange: [0, 300],
outputRange: [1, 0.5]
})
}
]}/>
</View>
)
}

插值的动画既能映射number值,也可以映射string值,这可以用于旋转动画

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

插值强大的地方在于它可以分段映射,比如下面表达式分了4段映射

1
2
3
4
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
1
2
3
4
5
6
7
8
9
10
11
12
Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0

滑动和拖动事件

滑动指的是ScrollView的滑动事件可以跟动画事件绑定,onScroll是ScrollView的滑动回调,可以使用Animated.event直接将contentOffset绑定在动画值上

1
2
3
4
5
6
7
8
9
10
11
12
13
const scrollX = useRef(new Animated.Value(0)).current

<ScrollView
onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}
/>

将scrollX绑定在top值上就可以实现滑动ScrollView控制方块下降

拖动指的是panResponder事件,本质上也是用AnimateEvent绑定事件

代码如下:

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
const PanEvenView: React.FC = () => {    
const pan = useRef(new Animated.ValueXY()).current
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
//绑定拖动事件
onPanResponderMove: Animated.event([null, {dx: pan.x, dy: pan.y}]),
onPanResponderRelease: () => {
Animated.spring(pan, {
toValue: {x: 0, y: 0},
useNativeDriver: true
}).start()
}
})
).current

return (
<View style={styles.container}>
<View style={styles.showZone}>
<Animated.View style={[styles.block, {transform: [
{
translateX: pan.x
},
{
translateY: pan.y
}
]}]}
{...panResponder.panHandlers}
/>
</View>
</View>
)
}

创建PanResponder,将Animated.event绑定在move事件即可,这里在pan release事件里面做了一个归位的spring动画,拖动方块释放后会以弹簧动画回到初始位置,上面Animated.View绑定的是translateX跟translateY,可以用top跟left平替,唯一的不同是translateX跟translateY可以通过开启useNativeDriver打开原生动画系统

LayoutAnimation

还有一类动画不是通过Animated api去实现的,而是将Flexbox的布局自动补间成动画,这种动画不需要我们去计算终止值,能简化动画的实现,唯一的缺点是它的定制化属性较少。

App中经常有这样的场景,一篇文章太长,底部有一个查看更多的按钮,点击完后,容纳文章的视图将拉长,使得文章得以全部展示。

1
2
3
4
5
<View style={[styles.bubble, {height}]}>
<Text style={styles.buttonText}>{
"dfsdf\ndfsd\ndfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdf"
}</Text>
</View>

RN中并不需要计算容器的高度去实现这件,而是通过将height从一个固定的number值,改成’auto’即可实现展开查看更多。

上面调用了setHeight(30) 收起弹窗,setHeight('auto')展开更多

而要将改动作改为动画,使用LayoutAnimation是最合适的

首先全局位置开启LayoutAnimation总开关

1
UIManager.setLayoutAnimationEnabledExperimental(true);

在布局动画的动作前调用LayoutAnimation的api

1
2
3
4
LayoutAnimation.easeInEaseOut();

//也可以是其他类型的动画,比如弹性动画
LayoutAnimation.spring();

完整代码

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import { LayoutAnimation, NativeModules } from "react-native";

//开启LayoutAnimation总开关
const {UIManager} = NativeModules
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);

interface AnimateFuncProps {
name: string,
func: () => void
}

const LayoutAnimationView: React.FC = () => {
const [height, setHeight] = useState<number|'auto'>(30)

const datas: AnimateFuncProps[] = [
{
name: 'reset',
func: () => {
//使用缓入缓出的动画效果
LayoutAnimation.easeInEaseOut();
setHeight(30)
}
},
{
name: 'start',
func: () => {
//使用缓入缓出的动画效果
LayoutAnimation.easeInEaseOut();
setHeight('auto')
}
},
]

return (
<View style={styles.container}>
<View style={styles.selector}>
{
datas.map((data) => {
let backgroundColor = 'green'
if (data.name == 'reset') {
backgroundColor = 'orange'
}
return (
<TouchableWithoutFeedback onPress={() => {
data.func()
}}>
<View style={[styles.button, {backgroundColor}]}>
<Text style={styles.buttonText}>{data.name}</Text>
</View>
</TouchableWithoutFeedback>
)
})
}
</View>
<View style={styles.showZone}>
<View style={[styles.bubble, {height}]}>
<Text style={styles.buttonText}>{
"dfsdf\ndfsd\ndfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdfdfsdf\ndfsdf"
}</Text>
</View>
</View>
</View>
)
}