想先推薦一下近期在寫的一個React Native項目,名字叫 Gakki :是一個 Mastodon的第三方客戶端 (Android App)
預覽html
原本我也不想造這個輪子的,奈何沒找到合適的組件。只能本身上了~react
思路很清楚: 監聽滾動事件,動態修改Header組件和Content組件的top值(固然,他們默認都是position:relative)。git
接下來實現的時候遇到了問題,我第一個版本是經過動態設置state來實現,即:github
/** * 每次滾動時,從新設置headerTop的值 */ onScroll = event =>{ const y = event.nativeEvent.contentOffset.y if (y >= 270) return // headerTop便是Header和Content的top樣式對應的值 this.setState({ headerTop: y }) }
這樣雖然能實現,可是效果很差:明顯能夠看到在上滑的過程當中,Header組件一卡一卡地向上方移動(一點都不流暢)。react-native
由於就只能另尋他法了:動畫api
React Native 提供了兩個互補的動畫系統:用於建立精細的交互控制的動畫Animated
和用於全局的佈局動畫LayoutAnimation
(筆者注:此次沒有用到它)
首先,這兒有一個簡單「逐漸顯示」動畫的DEMO,須要你先看完(文檔很簡單明瞭且註釋清楚,不必Copy過來)。函數
在看懂了DEMO的基礎上,咱們還須要瞭解兩個關鍵的API才能實現完整的效果:佈局
1. interpolateflex
插值函數。用來對不一樣類型的數值作映射處理。動畫
固然,這是文檔說明: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 { 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' } })