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

2次阅读

共计 3297 个字符,预计需要花费 9 分钟才能阅读完成。

想先推荐一下近期在写的一个 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’
}
})

正文完
 0