对于React Native,我想说入坑需谨慎
背景
最近做项目中有一个类似今日头条小视频左右滑动可以切换小视频的需求。对于这个需求如何实现,我首先想到的是用FlatList去解决,但是FlatList扩展性很差,不太适合。然后我想到了VirtualizedList去实现,想了想还是很麻烦,项目很急,自己去一点点写来不及。如果是用ScrollView去实现此功能,倒是比较容易,但是考虑到列表的数据量可能是成百上千条数据,即使再优化,数据量一多,App肯定卡的动不了。后来我发现react-native-swiper这个库,它有针对左右滑动长列表的优化,尝试了一下还可以,然后就使用了。功能很快完成了,但是当数据量达到200条以后,就明显感觉到卡顿了,App上线后用户反馈并不好。既然这些组件都不能很好的解决长列表的问题,那我自己写一个滑动组件。
效果
效果图思路
1、每次展示列表中的三条数据
2、三条数据插入方式如图,其实是5条数据,第一条和最后一条分别为第三条数据和第一条数据(随便一画有点难看):
图片插入方式
3、每一条数据都为屏幕宽度"const {width} = Dimensions.get('window')",总宽度度为5倍宽度"width * 5",当然这个宽度可以自定义。
4、首先展示第一条数据(数字为1的数据),若向做滑动到最后为1条数据的时候,在动画完成后,将位置重置为数字为1的地方,这样就实现了左滑功能,右滑动反之。
5、需要是用手势PanResponder与动画Animated,来实现滑动拖拽与动画效果
代码
import React, {Component} from 'react';
import {View, Animated, Dimensions, PanResponder, Image} from 'react-native';
const { width } = Dimensions.get('window')
class SwiperView extends Component {
constructor(props){
super(props);
this.state={
sports: new Animated.Value(-width), // 设置初始值
}
this.startTimestamp = 0 // 拖拽开始时间戳(用于计算滑动速度)
this.endTimestamp = 0 // 拖拽结束时间戳用于计算滑动速度)
this.page = 1 // 首次展示第一条数据(page 最小值为0,即从0开始,1为第二个条目)
}
componentWillMount () {
this.panResponder()
}
panResponder () {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderTerminationRequest: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
// 滑动开始,记录时间戳
this.startTimestamp = evt.nativeEvent.timestamp
},
onPanResponderMove: (evt, gestureState) => {
// 滑动横向距离
let x = gestureState.dx
// 实时改变滑动位置
if (x > 0) {
this.setState({
sports: new Animated.Value(-this.page * width + x)
})
} else {
this.setState({
sports: new Animated.Value(x - this.page * width)
})
}
},
onPanResponderRelease: (evt, gestureState) => {
// 滑动结束时间戳
this.endTimestamp = evt.nativeEvent.timestamp
// 滑动距离,根据滑动距离与时间戳计算是否切换到下一个条目
let x = gestureState.dx
if (x > 0) {
// 滑动距离大于屏幕1半,开启动画,滑动到下一个界面,或者滑动速度很快,并且滑动距离大于20,也滑动到下一个条目
if (x > width / 2 || (this.endTimestamp - this.startTimestamp < 300 && x > 20)) {
this.page -= 1
}
Animated.timing(
this.state.sports,
{
toValue: -this.page * width,
duration: 200
}
).start((state) => {
// 动画完成,判断是否需要重置位置
if (state.finished) {
if (this.page <= 0) {
this.page = 3
this.setState({
sports: new Animated.Value(-3 * width)
})
}
}
});
} else {
x = Math.abs(x)
// 滑动距离大于屏幕1半,开启动画,滑动到下一个界面,或者滑动速度很快,并且滑动距离大于20,也滑动到下一个条目
if (x > width / 2 || (this.endTimestamp - this.startTimestamp < 300)) {
this.page += 1
}
Animated.timing(
this.state.sports,
{
toValue: -this.page * width,
duration: 200
}
).start((state) => {
// 动画完成,判断是否需要重置位置
if (state.finished) {
if (this.page >= 4) {
this.page = 1
this.setState({
sports: new Animated.Value(-width * this.page)
})
}
}
});
}
},
onShouldBlockNativeResponder: (evt, gestureState) => {
return false
}
})
}
render(){
return (
<Animated.View
style={{...this.props.style, left:this.state.sports}}
{...this._panResponder.panHandlers}
>
{this.props.children}
</Animated.View>
);
}
}
export default class App extends Component {
render() {
return (
<View style={[{width:width,height:'100%'}]}>
<SwiperView style={{width:width * 4,height:'100%',flexDirection:"row"}}>
<Image source={require('./assets/3.jpeg')} style={[{width,height:'100%',backgroundColor:"#FFF"}]} />
<Image source={require('./assets/1.jpeg')} style={[{width,height:'100%',backgroundColor:"red"}]} />
<Image source={require('./assets/2.jpeg')} style={[{width,height:'100%',backgroundColor:"green"}]} />
<Image source={require('./assets/3.jpeg')} style={[{width,height:'100%',backgroundColor:"#FFF"}]} />
<Image source={require('./assets/1.jpeg')} style={[{width,height:'100%',backgroundColor:"red"}]} />
</SwiperView>
</View>
);
}
}
总结
1、这只是实现需求的第一步,后续会继续优化、封装,达到想要的效果
2、如果只想做banner轮播图展示,将手势那一块替换为setInterval就可以了。
3、如果有更好的思路欢迎交流
网友评论