以前我没得选,现在我只想做个坏人

ReactNative: 使用Animted API实现向上滚动时隐藏Header组件

    大前端     ReactNative·AnimtedAPI·动画

  1. #写在前面
  2. #Animated 相关API介绍
  3. #完整代码

想先推荐一下近期在写的一个React Native项目,名字叫 Gakki :是一个Mastodon的第三方客户端 (Android App)

预览

不见图请翻墙

#写在前面


本来我也不想造这个轮子的,奈何没找到合适的组件。只能自己上了~

思路很清楚: 监听滚动事件,动态修改Header组件和Content组件的top值(当然,他们默认都是position:relative)。

接下来实现的时候遇到了问题,我第一个版本是通过动态设置state来实现,即:

1
2
3
4
5
6
7
8
9
10
11
/**
* 每次滚动时,重新设置headerTop的值
*/
onScroll = event =>{
const y = event.nativeEvent.contentOffset.y
if (y >= 270) return
// headerTop即是Header和Content的top样式对应的值
this.setState({
headerTop: y
})
}

这样虽然能实现,但是效果不好:明显可以看到在上滑的过程中,Header组件一卡一卡地向上方移动(一点都不流畅)。

因为就只能另寻他法了:动画

React Native 提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation (笔者注:这次没有用到它)

#Animated 相关API介绍


首先,这儿有一个简单“逐渐显示”动画的DEMO,需要你先看完(文档很简单明了且注释清楚,没必要Copy过来)。

在看懂了DEMO的基础上,我们还需要了解两个关键的API才能实现完整的效果:

1. interpolate

插值函数。用来对不同类型的数值做映射处理。

当然,这是文档说明:

Each property can be run through an interpolation first. An interpolation maps input ranges to output ranges, typically using a linear interpolation but also supports easing functions. By default, it will extrapolate the curve beyond the ranges given, but you can also have it clamp the output value.

翻译:

每个属性可以先经过插值处理。插值对输入范围和输出范围之间做一个映射,通常使用线性插值,但也支持缓和函数。默认情况下,如果给定数据超出范围,他也可以自行推断出对于的曲线,但您也可以让它箝位输出值(P.S. 最后一句可能翻译错误,因为没搞懂clamp value指的是什么, sigh…)

举个例子:

在实现一个图片旋转动画时,输入值只能是这样的:

1
2
3
4
5
6
7
8
9
10
11
this.state = {
rotate: new Animated.Value(0) // 初始化用到的动画变量
}

...

// 这么映射是因为style样式需要的是0deg这样的值,你给它0这样的值,它可不能正常工作。因此必定需要一个映射处理。
this.state.rotate.interpolate({ // 将0映射成0deg,1映射成360deg。当然中间的数据也是如此映射。
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})

2. Animated.event

一般动画的输入值都是默认设定好的,比如前面DEMO中的逐渐显示动画中的透明度:开始是0,最后是1。这是已经写死了的。

但如果有些动画效果需要的不是写死的值,而是动态输入的呢,比如:手势(上滑、下滑,左滑,右滑…)、其它事件。

那就用到了Animated.event

直接看一个将滚动事件的y值(滚动条距离顶部高度)和我们的动画变量绑定起来的例子:

1
2
3
4
5
6
7
8
9
// 这段代码表示:在滚动事件触发时,将event.nativeEvent.contentOffset.y 的值动态绑定到this.state.headerTop上
// 和最前面我通过this.setState动态设置的目的一样,但交给Animated.event做就不会造成视觉上的卡顿了。
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: { y: this.state.headerTop }
}
}
])}

关于API更多的说明请移步文档

#完整代码


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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import React, { Component } from 'react'
import { StyleSheet, Text, View, Animated, FlatList } from 'react-native'

class List extends Component {
render() {
// 模拟列表数据
const mockData = [
'富强',
'民主',
'文明',
'和谐',
'自由',
'平等',
'公正',
'法治',
'爱国',
'敬业',
'诚信',
'友善'
]

return (
<FlatList
onScroll={this.props.onScroll}
data={mockData}
renderItem={({ item }) => (
<View style={styles.list}>
<Text>{item}</Text>
</View>
)}
/>
)
}
}

export default class AnimatedScrollDemo extends Component {
constructor(props) {
super(props)
this.state = {
headerTop: new Animated.Value(0)
}
}

componentWillMount() {
// P.S. 270,217,280区间的映射是告诉interpolate,所有大于270的值都映射成-50
// 这样就不会导致Header在上滑的过程中一直向上滑动了
this.top = this.state.headerTop.interpolate({
inputRange: [0, 270, 271, 280],
outputRange: [0, -50, -50, -50]
})

this.animatedEvent = Animated.event([
{
nativeEvent: {
contentOffset: { y: this.state.headerTop }
}
}
])
}

render() {
return (
<View style={styles.container}>
<Animated.View style={{ top: this.top }}>
<View style={styles.header}>
<Text style={styles.text}>linshuirong.cn</Text>
</View>
</Animated.View>
{/* 在oHeader组件上移的同时,列表容器也需要同时向上移动,需要注意。 */}
<Animated.View style={{ top: this.top }}>
<List onScroll={this.animatedEvent} />
</Animated.View>
</View>
)
}
}

const styles = StyleSheet.create({
container: {
flex: 1
},
list: {
height: 80,
backgroundColor: 'pink',
marginBottom: 1,
alignItems: 'center',
justifyContent: 'center',
color: 'white'
},
header: {
height: 50,
backgroundColor: '#3F51B5',
alignItems: 'center',
justifyContent: 'center'
},
text: {
color: 'white'
}
})
页阅读量:  ・  站访问量:  ・  站访客数: