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