用RN实现下拉筛选的效果:先上效果
这里主要用到几个效果:(我是个初学者,有不对的地方或者有好的方法请指出,谢谢)
1. 箭头的旋转
2. 背景的渐变
3. FlatList的高度变化
4. 以及最后选中项目以后把所有的效果重置
一、箭头的旋转
<Button title="全部" style={{color:'black'}} onPress={() => {
this.startAnimation(i)
}}> </Button>
<Animated.Image source={require('../../../images/fast24.png')} style={{
transform: [
{
rotateZ: this.state.rotations[i].interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg']
})
}
],
}}>
</Animated.Image>
RN动画首先要初始化一个new Animated.Value(0)
,然后把这个state
绑定到style
相应的样式中,因为我是三个列表,我这里存储的是数组。但是RN只有Animated.View
Animated.Image
Animated.Text
几个可以用,应该够用了
摘抄一下:
一个RN的动画,可以按照以下步骤进行。
- 使用基本的Animated组件,如
Animated.View
Animated.Image
Animated.Text
- 使用Animated.Value设定一个或多个初始化值(透明度,位置等等)。
- 将初始化值绑定到动画目标的属性上(如style)
- 通过Animated.timing等函数设定动画参数
调用start启动动画
初始化的部分代码
constructor(props) {
super(props)
this.state = {
/**背景变化的动画 opacity*/
fadeView: new Animated.Value(0),
showMask: false,
/**flatlist高度变化的动画*/
rotateHeight: new Animated.Value(0),
/**flatlist显示的高度(分别计算所得)*/
flatHeight: 0,
/**各个按钮的动画*/
rotations: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)],
rotationColors: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)],
/**当前Flatlist显示的数据*/
dataArray: [],
/**选中的数据 index*/
dataSelectArray: [0, 0, 0],
}
if (this.props.centerArray) {
this.state.dataArray = this.props.centerArray
} else if (nextProps.leftArray) {
this.state.dataArray = this.props.leftArray
} else if (nextProps.rightArray) {
this.state.dataArray = this.props.rightArray
}
this.calFlagHeight()
}
calFlagHeight() {
/**取数组中length的最大值*/
let max = Math.max(this.props.leftArray.length, this.props.centerArray.length)
this.state.flatHeight = Math.max(Math.max(this.props.leftArray.length, this.props.centerArray.length), this.props.rightArray.length) * this.props.renderItemHeight
}
把Animated
的值绑定到Animated.Image
样式transform
上面,
interpolate
这个函数就是一个值得映射,把0
对应成0deg
,1
对应成180deg
,后面还要用它映射列表的滚动高度。好了,接下来就要实现旋转效果。
<Button title="全部" style={{color:'black'}} onPress={() => {
this.startAnimation(i)
}}> </Button>
<Animated.Image source={require('../../../images/fast24.png')} style={{
transform: [
{
rotateZ: this.state.rotations[i].interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg']
})
}
],
}}>
</Animated.Image>
当点击按钮的时候,执行动画效果,因为我需要知道箭头向下还是向下我才能判断,箭头该怎么旋转。怎么判断呢?我想通过Animated.Value
的值来判断。因为Value==0
的时候一定是向下的箭头。果然有这么个方法,__getValue()
这个方法就是获取这个Value
。所以代码里面判断0
或者1
来决定箭头的旋转.
start()
方法开始执行动画,在回调里面就是动画执行完成,可以搞一些事情
//只是部分代码。完整代码Git上面有
Animated.timing(this.state.rotations[index], {
toValue: this.state.rotations[index].__getValue() == 0 ? 1 : 0,
duration: 1000,
easing: Easing.linear
}).start(),
旋转完了以后,其他的效果就一样了,下面是绑定列表高度和背景透明度变化的代码,列表高度我是取三个列表中最大值。上面这个方法calFlagHeight()
计算高度
<Animated.View style={styles.container} style={[styles.container, {
opacity: this.state.fadeView,
}]}>
</Animated.View>
<Animated.View style={[styles.flatlist, {
height: this.state.rotateHeight.interpolate({
inputRange: [0, 1],
outputRange: [0, this.state.flatHeight]
})
}]} >
<FlatList
data={this.state.dataArray}
renderItem={this._renderItemUI}
/>
</Animated.View>
三个分别的动画实现以后就需要合在执行动画了,总不能一个一个执行吧。RN提供了同时执行动画的方法。这个方法Animated.parallel()
就是同时执行数组里面的动画。
1. 当背景显示以后就不需要再次执行背景渐变的动画,所以需要记录背景的状态showMask
,
2. 背景显示以后,当我点击第一个按钮以后,列表显示,我又点击第二个按钮,这是第一个按钮的箭头Value=1
,我需要把第一个按钮Value=0
,
3. 列表高度变化的动画时间我设置的比其他的长,效果更好一点(一样长高度变化看不到什么效果,自己决定)
4. start()
回调里面我写了两个状态showMask
是控制背景,还有一个是isAnimaing
,因为如果多次点击按钮的时候上次动画没执行完就再次执行新的动画(应该还有其他的方法控制)所以加个判断,都执行完以后再执行新的动画
5. 选中列表项目以后,要把所有状态重置(背景消失,箭头回归)把所有状态重置就好了Value=0
,showMask=false
下面是PopView.js完整代码
6. 用到了react-native-elements
这个库,这个可以去掉,用自带的Button
就可以,
获取高度可以自己改一下import { screenWidth, screenHeight } from '../../utils/Utils';
popView.js git项目地址
import React, { Component } from 'react'
import { View, Text, Image, FlatList, StyleSheet, Animated, Easing, TouchableOpacity } from 'react-native'
import PropTypes from 'prop-types';
import { Button } from 'react-native-elements';
import { screenWidth, screenHeight } from '../../utils/Utils';
/**防止重复点击执行动画*/
let isAnimaing = false
export default class PopView extends Component {
static defaultProps = {
topHeight: 40,
renderItemHeight: 35,
}
constructor(props) {
super(props)
this.state = {
/**背景变化的动画 opacity*/
fadeView: new Animated.Value(0),
showMask: false,
/**flatlist高度变化的动画*/
rotateHeight: new Animated.Value(0),
/**flatlist显示的高度(分别计算所得)*/
flatHeight: 0,
/**各个按钮的动画*/
rotations: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)],
rotationColors: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)],
/**当前Flatlist显示的数据*/
dataArray: [],
/**选中的数据 index*/
dataSelectArray: [0, 0, 0],
}
if (this.props.centerArray) {
this.state.dataArray = this.props.centerArray
} else if (nextProps.leftArray) {
this.state.dataArray = this.props.leftArray
} else if (nextProps.rightArray) {
this.state.dataArray = this.props.rightArray
}
this.calFlagHeight()
}
calFlagHeight() {
/**取数组中length的最大值*/
let max = Math.max(this.props.leftArray.length, this.props.centerArray.length)
this.state.flatHeight = Math.max(Math.max(this.props.leftArray.length, this.props.centerArray.length), this.props.rightArray.length) * this.props.renderItemHeight
}
componentWillReceiveProps(nextProps) {
if (nextProps.centerArray) {
this.state.dataArray = nextProps.centerArray
} else if (nextProps.leftArray) {
this.state.dataArray = nextProps.leftArray
} else if (nextProps.rightArray) {
this.state.dataArray = nextProps.rightArray
}
this.calFlagHeight()
}
//点击按钮执行动画
startAnimation(index) {
if (isAnimaing) {//动画执行中不再执行
return
}
isAnimaing = true //动画执行中
switch (index) {
case 0: {
this.setState({
dataArray: this.props.leftArray
})
} break
case 1: {
this.setState({
dataArray: this.props.centerArray
})
} break
case 2: {
this.setState({
dataArray: this.props.rightArray
})
} break
default: {
}
}
this.calFlagHeight()
if (this.state.showMask) {
// this.state.rotations[index].setValue(0)
// this.state.rotateHeight.setValue(0)
// this.state.rotationColors[index].setValue(0)
//同时执行的动画
Animated.parallel([
//判断旋转和高度该怎么变化
Animated.timing(this.state.rotations[index], {
toValue: this.state.rotations[index].__getValue() == 0 ? 1 : 0,
duration: 1000,
easing: Easing.linear
}),
Animated.timing(this.state.rotationColors[index], {
toValue: this.state.rotationColors[index].__getValue() == 0 ? 1 : 0,
duration: 1000,
easing: Easing.linear
}),
Animated.spring(this.state.rotateHeight, {
toValue: this.state.rotateHeight.__getValue() == 0 ? 1 : 0,
duration: 1500,
friction: 40
})
]).start(() => isAnimaing = false)
} else {
this.state.showMask = true
this.state.fadeView.setValue(0)
this.state.rotations[index].setValue(0)
this.state.rotateHeight.setValue(0)
this.state.rotationColors[index].setValue(0)
console.log('fadeview' + this.state.fadeView.Value)
Animated.parallel([
Animated.timing(this.state.rotations[index], {
toValue: 1,
duration: 1000,
easing: Easing.linear
}),
Animated.timing(this.state.rotationColors[index], {
toValue: 1,
duration: 1000,
easing: Easing.linear
}),
Animated.timing(this.state.fadeView, {
toValue: 0.4,
duration: 1000,
easing: Easing.linear
}),
Animated.spring(this.state.rotateHeight, {
toValue: 1,
duration: 1500,
friction: 40
})
]).start(() => isAnimaing = false)//动画执行完了
}
}
//选择项目以后执行的动画(重置)
clearAnimation() {
if (this.state.showMask == false||isAnimaing==true) {
return
}
Animated.parallel([
this.state.rotations.forEach((value, key) => {
Animated.timing(value, {
toValue: 0,
duration: 1000,
easing: Easing.linear
})
}),
this.state.rotationColors.forEach((value, key) => {
Animated.timing(value, {
toValue: 0,
duration: 1000,
easing: Easing.linear
})
}),
Animated.timing(this.state.fadeView, {
toValue: 0,
duration: 1000,
easing: Easing.linear
}),
Animated.spring(this.state.rotateHeight, {
toValue: 0,
duration: 1500,
friction: 40
})
]).start(()=>{
isAnimaing = false
this.state.showMask = false
})
}
getFlatListStyle() {
// const {}
}
_renderItemUI = ({ item, index }) => {
return (
<TouchableOpacity onPress={() => {
if (this.props.selectCallBack) {
this.state.dataSelectArray.push(index)
this.props.selectCallBack(this.state.dataSelectArray)
}
this.clearAnimation()
}}>
<View style={{ width: screenWidth(), height: 30 }}>
<Text>{item}</Text>
</View>
</TouchableOpacity>
)
}
renderTopTitles() {
let titlesView = []
for (let i = 0; i < 3; i++) {
titlesView.push(<View style={{width:screenWidth()/3.0,height:'100%',display:'flex',flexDirection:'row',alignItems:'center'}}>
<Button title="全部" style={{color:'black'}} onPress={() => {
this.startAnimation(i)
}}> </Button>
<Animated.Image source={require('../../../images/fast24.png')} style={{
transform: [
{
rotateZ: this.state.rotations[i].interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg']
})
}
],
}}>
</Animated.Image>
</View>)
}
return titlesView
}
renderTopView() {
return (
<View>
<View style={styles.tapTitle}>
{this.renderTopTitles()}
</View>
<Animated.View style={styles.container} style={[styles.container, {
opacity: this.state.fadeView,
}]}>
</Animated.View>
<Animated.View style={[styles.flatlist, {
height: this.state.rotateHeight.interpolate({
inputRange: [0, 1],
outputRange: [0, this.state.flatHeight]
})
}]} >
<FlatList
data={this.state.dataArray}
renderItem={this._renderItemUI}
/>
</Animated.View>
</View>
)
}
render() {
return (
<View >
{this.renderTopView()}
</View>
)
}
}
PopView.propTypes = {
topHeight: PropTypes.number,
renderItemHeight: PropTypes.number,
leftArray: PropTypes.array,
centerArray: PropTypes.array,
rightArray: PropTypes.array,
selectCallBack: PropTypes.func,
}
const styles = StyleSheet.create({
container: {
width: screenWidth(),
height: screenHeight() - 40,
opacity: 0,
backgroundColor: 'black',
zIndex: 100,
position: 'absolute',
left: 0,
top: 40
},
tapTitle: {
width: screenWidth(),
height: 40,
display: 'flex',
position: 'absolute',
left: 0,
top: 0,
flexDirection: 'row',
alignItems: 'center'
},
flatlist: {
display: 'flex',
position:'absolute',
left:0,
top:45,
width: screenWidth(),
backgroundColor: 'orange',
zIndex:102,
},
triangle: {
width: 0,
height: 0,
borderLeftWidth: 10,
borderLeftColor: 'transparent',
borderRightWidth: 10,
borderRightColor: 'transparent',
borderStyle: 'solid',
borderTopWidth: 20,
borderTopColor: 'red'
}
})