前言: 一周又過去了,一直在趕需求,除了自己利用空餘時間學習一下外壓根就沒時間去研究新東西,唉~程式猿就是這樣,活到老學到老!! 廢話不多說了,公司產品覺得某電商的商品詳情頁面很nice,問我們能不能實現(寶寶心裡苦),於是研究了一波,下面把我研究的東西分享一下,小夥伴有啥好的實現方式還謝謝分享一下哦~拜謝!
先看一下最終實現的效果:
ios/android
簡單來說就是分為兩個部分(上下),兩個都是(scrollview、flatlist)等滑動組件,第一個scrollview滑動到底部的時候,繼續上拉顯示第二個scrollview,第二個scrollview下拉到頂部的時候,繼續下拉回到第一個scrollview並且第一個scrollview回到頂部.
下面說一下我的大體思路:
第一種方式:
利用rn手勢,然後監聽scrollview的滑動事件,我們都知道rn中的事件傳遞都是從父控制項一層一層往下傳遞,所以父控制項能夠攔截scrollview也就是子控制項的能力,當scrollview滑動到底部或者頂部的時候攔截scrollview的事件,把事件給父控制項,然後通過控制父控制項的垂直位移量首先分頁功能,說了那麼多理論的東西小夥伴估計都累了,我們後面結合代碼一起來說一下.
第二種方式:
封裝一個native的組件實現上拉和下拉的操作,rn只需要做一些簡單的監聽就可以.
兩種方式對比來看,能用rn實現最好,因為rn存在的目的也就是為了實現跨平台目的,都用native實現了還用rn幹嘛!! 話雖然這樣說,但是rn還是給開發人員提供了自訂的方法,用第二種方式實現有點就是效能和體驗上要優於rn實現,我接下來會結合兩種方式來實現.
先說一下第一種方式
第一步(把頁面分為上下兩部分):
render() { return ( <View style={[styles.container]} > {/*第一部分*/} <Animated.View style={[styles.container1,{ marginTop:this._aniBack.interpolate({ inputRange:[0,1], outputRange:[0,-SCREEN_H], }) }]} {...this._panResponder.panHandlers} > <Animated.View ref={(ref) => this._container1 = ref} style={{width: SCREEN_W, height: SCREEN_H, marginTop:this._aniBack1.interpolate({ inputRange:[0,1], outputRange:[0,-100], })}} > <ScrollView ref={(ref)=>this._scroll1=ref} bounces={false} scrollEventThrottle={10} onScroll={this._onScroll.bind(this)} overScrollMode={'never'} > {this._getContent()} </ScrollView> </Animated.View> <View style={{width:SCREEN_W,height:100,backgroundColor:'white',alignItems:'center',justifyContent:'center'}}> <Text>上拉查看詳情</Text> </View> </Animated.View> {/*第二部分*/} <View style={styles.container2} {...this._panResponder2.panHandlers} > <Animated.View ref={(ref) => this._container2 = ref} style={{ width:SCREEN_W,height:100,backgroundColor:'white',alignItems:'center',justifyContent:'center', marginTop:this._aniBack2.interpolate({ inputRange:[0,1], outputRange:[-100,0], }) }} > <Text>下拉回到頂部</Text> </Animated.View> <View style={{width: SCREEN_W, height: SCREEN_H,}} > <ScrollView ref={(ref)=>this._scroll2=ref} bounces={false} scrollEventThrottle={10} onScroll={this._onScroll2.bind(this)} overScrollMode={'never'} > {this._getContent()} </ScrollView> </View> </View> </View> ); }
代碼我待會會貼出來,原理很簡單,我大體說一下我的實現思路,運行代碼你會發現,頁面是顯示了一個紅色頁面(也就是上部分).
讓我們把第一個頁面的marginTop調為-SCREEN_H(螢幕高度)的時候,我們會看到第二屏藍色頁面
所以我們只需要在第一個紅色頁面的scrollview滑動到底部的時候,然後攔截事件,手指抬起的時候,讓第一個頁面的marginTop從(0到-螢幕高度)的轉變,我們同時給個動畫實現.那麼問題來了,我們該怎麼監聽scrollview到達頂部或者底部呢?我們又該怎麼攔截scrollview的事件呢?
監聽scrollview到達頂部或者底部:
到達頂部我們都知道,當scrollview的y軸位移量=0的時候我們就認為scrollview到達頂部了,轉為代碼就是:
_onScroll2(event){ this._reachEnd2=false; let y = event.nativeEvent.contentOffset.y; if(y<=0){ //到達頂部了 this._reachEnd2=true; } }
到達底部也就是當(子控制項的高度=y軸滑動的距離+父控制項的高度)的時候,轉為代碼為:
_onScroll(event){ this._reachEnd1=false; let y = event.nativeEvent.contentOffset.y; let height = event.nativeEvent.layoutMeasurement.height; let contentHeight = event.nativeEvent.contentSize.height; if (contentHeight > height && (y + height >= contentHeight)) { //到達頂部了 this._reachEnd1=true; } }
父控制項攔截子控制項的事件:
我們在onMoveShouldSetPanResponderCapture返回true,父控制項就是攔截掉滑動事件,然後交給自己處理(onPanResponderMove),那麼我們紅色頁面(也就是第一頁)的scrollview到達底部的時候,再往上拉的時候,我們攔截事件
_handleMoveShouldSetPanResponderCapture(event: Object, gestureState: Object,): boolean { console.log('_handleMoveShouldSetPanResponderCapture'); console.log(gestureState.dy); //當滑動到底部並且繼續往上拉的時候 return this._reachEnd1&&gestureState.dy<0; }
我們第二個頁面(也就是藍色頁面)當scrollview滑動到頂部並且繼續往下拉的時候,攔截事件:
_handleMoveShouldSetPanResponderCapture2(event: Object, gestureState: Object,): boolean { console.log(gestureState.dy); console.log('_handleMoveShouldSetPanResponderCapture2'); //當滑動到頂部並且繼續往下拉的時候 return this._reachEnd2&&gestureState.dy>=0; }
好啦~我們第一個頁面的父控制項拿到滑動事件後,我們繼續往上拉,也就是把往上拉的距離賦給我們的“上拉查看詳情“組件了:
_handlePanResponderMove(event: Object, gestureState: Object): void {
//防止事件攔截不準,我們把scrollview的scrollEnabled:false設定為false
this._scroll1.setNativeProps({
scrollEnabled:false
})
let nowLeft =gestureState.dy*0.5;
//控制一個頁面的“上拉查看詳情“組件顯示
this._container1.setNativeProps({
marginTop:nowLeft
})
console.log(‘_handlePanResponderMove’,gestureState.dy);
<Animated.View ref={(ref) => this._container1 = ref} style={{width: SCREEN_W, height: SCREEN_H, marginTop:this._aniBack1.interpolate({ inputRange:[0,1], outputRange:[0,-100], })}} > <ScrollView ref={(ref)=>this._scroll1=ref} bounces={false} scrollEventThrottle={10} onScroll={this._onScroll.bind(this)} overScrollMode={'never'} > {this._getContent()} </ScrollView> </Animated.View> <View style={{width:SCREEN_W,height:100,backgroundColor:'white',alignItems:'center',justifyContent:'center'}}> <Text>上拉查看詳情</Text> </View>
代碼很簡單,我就不一一解釋了,小夥伴自己去運行看看哈,下面是第一種方式的所有實現代碼,直接運行就可以了:
/** * Sample React Native App * https://github.com/facebook/react-native * @flow */import React, {Component} from 'react';import { Platform, StyleSheet, Text, View, TouchableOpacity, Dimensions, ScrollView, PanResponder, Animated, StatusBar,} from 'react-native';const SCREEN_W = Dimensions.get('window').width;const SCREEN_H = Dimensions.get('window').height-(Platform.OS==='android'?StatusBar.currentHeight:0);import SwipeRow from './SwipeRow';export default class App extends Component { // 構造 constructor(props) { super(props); // 初始狀態 // 初始狀態 this._panResponder = PanResponder.create({ onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture.bind(this), onStartShouldSetResponderCapture:this._handleMoveShouldSetPanResponderCapture.bind(this), onPanResponderMove: this._handlePanResponderMove.bind(this), onPanResponderRelease: this._handlePanResponderEnd.bind(this), onPanResponderGrant:this._handlePanGrant.bind(this), onPanResponderTerminate:()=>{ console.log('onPanResponderTerminate'); this._aniBack1.setValue(0); Animated.spring(this._aniBack1,{ toValue:1 }).start(()=>{ this._handlePanResponderEnd(); }); }, onShouldBlockNativeResponder: (event, gestureState) => false,//表示是否用 Native 平台的事件處理,預設是禁用的,全部使用 JS 中的事件處理,注意此函數目前只能在 Android 平台上使用 }); this._panResponder2 = PanResponder.create({ onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture2.bind(this), onStartShouldSetResponderCapture:this._handleMoveShouldSetPanResponderCapture2.bind(this), onPanResponderMove: this._handlePanResponderMove2.bind(this), onPanResponderRelease: this._handlePanResponderEnd2.bind(this), onPanResponderGrant:this._handlePanGrant2.bind(this), onPanResponderTerminate:()=>{ this._container2.setNativeProps({ marginTop:0 }) this._aniBack2.setValue(0); Animated.spring(this._aniBack2,{ toValue:1 }).start(()=>{ this._handlePanResponderEnd2(); }); console.log('onPanResponderTerminate2'); }, onShouldBlockNativeResponder: (event, gestureState) => false,//表示是否用 Native 平台的事件處理,預設是禁用的,全部使用 JS 中的事件處理,注意此函數目前只能在 Android 平台上使用 }); this._reachEnd1=false; this._reachEnd2=true; this._aniBack=new Animated.Value(0); this._aniBack1=new Animated.Value(0); this._aniBack2=new Animated.Value(0); } _handlePanGrant(event: Object, gestureState: Object,){ this._scroll1.setNativeProps({ scrollEnabled:false }) } _handleMoveShouldSetPanResponderCapture(event: Object, gestureState: Object,): boolean { console.log('_handleMoveShouldSetPanResponderCapture'); console.log(gestureState.dy); return this._reachEnd1&&gestureState.dy<0; } _handlePanResponderMove(event: Object, gestureState: Object): void { this._scroll1.setNativeProps({ scrollEnabled:false }) let nowLeft =gestureState.dy*0.5; this._container1.setNativeProps({ marginTop:nowLeft }) console.log('_handlePanResponderMove',gestureState.dy); } _handlePanResponderEnd(event: Object, gestureState: Object): void { this._aniBack.setValue(0); this._scroll1.setNativeProps({ scrollEnabled: true }) this._scroll1.scrollTo({y:0},true); Animated.timing(this._aniBack, { duration: 500, toValue: 1 }).start(); this._aniBack1.setValue(1); Animated.spring(this._aniBack1, { toValue: 0 }).start(); } _handleMoveShouldSetPanResponderCapture2(event: Object, gestureState: Object,): boolean { console.log(gestureState.dy); console.log('_handleMoveShouldSetPanResponderCapture2'); return this._reachEnd2&&gestureState.dy>=0; } _handlePanResponderMove2(event: Object, gestureState: Object): void { console.log('_handlePanResponderMove2'); let nowLeft =gestureState.dy*0.5; this._scroll2.setNativeProps({ scrollEnabled:false }) this._container2.setNativeProps({ marginTop:-100+nowLeft }) console.log('_handlePanResponderMove2',gestureState.dy); } _handlePanGrant2(event: Object, gestureState: Object,){ this._scroll2.setNativeProps({ scrollEnabled:false }) } _handlePanResponderEnd2(event: Object, gestureState: Object): void { this._aniBack.setValue(1); this._scroll2.setNativeProps({ scrollEnabled: true }) Animated.timing(this._aniBack, { duration: 500, toValue: 0 }).start(); this._aniBack2.setValue(1); Animated.spring(this._aniBack2, { toValue: 0 }).start(); } render() { return ( <View style={[styles.container]} > {/*第一部分*/} <Animated.View style={[styles.container1,{ marginTop:this._aniBack.interpolate({ inputRange:[0,1], outputRange:[0,-SCREEN_H], }) }]} {...this._panResponder.panHandlers} > <Animated.View ref={(ref) => this._container1 = ref} style={{width: SCREEN_W, height: SCREEN_H, marginTop:this._aniBack1.interpolate({ inputRange:[0,1], outputRange:[0,-100], })}} > <ScrollView ref={(ref)=>this._scroll1=ref} bounces={false} scrollEventThrottle={10} onScroll={this._onScroll.bind(this)} overScrollMode={'never'} > {this._getContent()} </ScrollView> </Animated.View> <View style={{width:SCREEN_W,height:100,backgroundColor:'white',alignItems:'center',justifyContent:'center'}}> <Text>上拉查看詳情</Text> </View> </Animated.View> {/*第二部分*/} <View style={styles.container2} {...this._panResponder2.panHandlers} > <Animated.View ref={(ref) => this._container2 = ref} style={{ width:SCREEN_W,height:100,backgroundColor:'white',alignItems:'center',justifyContent:'center', marginTop:this._aniBack2.interpolate({ inputRange:[