想先推薦一下近期在寫的一個React Native項目,名字叫 Gakki :是一個Mastodon的第三方客戶端 (Android App)
預覽
寫在前面
本來我也不想造這個輪子的,奈何沒找到合適的組件。只能自己上了~
思路很清楚: 監聽滾動事件,動態修改Header組件和Content組件的top值(當然,他們默認都是position:relative)。
接下來實現的時候遇到了問題,我第一個版本是通過動態設置state來實現,即:
/**
* 每次滾動時,重新設置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...)
舉個例子:
在實現一個圖片旋轉動畫時,輸入值只能是這樣的:
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值(滾動條距離頂部高度)和我們的動畫變量綁定起來的例子:
// 這段代碼表示:在滾動事件觸發時,將event.nativeEvent.contentOffset.y 的值動態綁定到this.state.headerTop上
// 和最前面我通過this.setState動態設置的目的一樣,但交給Animated.event做就不會造成視覺上的卡頓了。
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: { y: this.state.headerTop }
}
}
])}
關於API更多的說明請移步文檔
完整代碼
import React, { Component } from 'react'
import { StyleSheet, Text, View, Animated, FlatList } from 'react-native'
class List extends Component {
onScroll = event => {
// 顯示和隱藏Header組件的動畫在 滾動條距離頂部距離小於270 時起作用
// 移除這個限制就是另一種效果了,可以自己想一想
if (this.props.onScroll) {
if (event.nativeEvent.contentOffset.y >= 270) return
this.props.onScroll(event)
}
}
render() {
// 模擬列表數據
const mockData = [
'富強',
'民主',
'文明',
'和諧',
'自由',
'平等',
'公正',
'法治',
'愛國',
'敬業',
'誠信',
'友善'
]
return (
<FlatList
onScroll={this.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)
}
}
onScroll = event => {
if (event.nativeEvent.contentOffset.y >= 270) return
Animated.event([
{ nativeEvent: { contentOffset: { y: this.state.headerTop } } }
])
}
render() {
const top = this.state.headerTop.interpolate({
inputRange: [0, 270],
outputRange: [0, -50]
})
return (
<View style={styles.container}>
<Animated.View style={{ top: top }}>
<View style={styles.header}>
<Text style={style.text}>linshuirong.cn</Text>
</View>
</Animated.View>
{/* 在oHeader組件上移的同時,列表容器也需要同時向上移動,需要注意下。 */}
<Animated.View style={{ top: top }}>
<List
onScroll={Animated.event([
{
nativeEvent: {
contentOffset: { y: this.state.headerTop }
}
}
])}
/>
</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'
}
})