對于React Native,我想說入坑需謹慎
背景
最近做項目中有一個類似今日頭條小視頻左右滑動可以切換小視頻的需求。對于這個需求如何實現,我首先想到的是用FlatList去解決,但是FlatList擴展性很差,不太適合。然后我想到了VirtualizedList去實現,想了想還是很麻煩,項目很急,自己去一點點寫來不及。如果是用ScrollView去實現此功能,倒是比較容易,但是考慮到列表的數據量可能是成百上千條數據,即使再優(yōu)化,數據量一多,App肯定卡的動不了。后來我發(fā)現react-native-swiper這個庫,它有針對左右滑動長列表的優(yōu)化,嘗試了一下還可以,然后就使用了。功能很快完成了,但是當數據量達到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、這只是實現需求的第一步,后續(xù)會繼續(xù)優(yōu)化、封裝,達到想要的效果
2、如果只想做banner輪播圖展示,將手勢那一塊替換為setInterval就可以了。
3、如果有更好的思路歡迎交流