ReactNative: 使用Animted API實現向上滾動時隱藏Header組件

想先推薦一下近期在寫的一個React Native項目,名字叫 Gakki :是一個Mastodon的第三方客戶端 (Android App)

預覽

rn.gif

寫在前面


本來我也不想造這個輪子的,奈何沒找到合適的組件。只能自己上了~

思路很清楚: 監聽滾動事件,動態修改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'
  }
})
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章