說道這裏,我嘗試着寫了個測試js例子,最外面套一個ReactNative自帶的ScrollView
並設置視頻播放控件
的高度爲200
和 Tab導航控件
的style={{height: windowHeight- 80}}
,那這樣滾動距離到120時,滾動條到底部了,視頻播發控件的區域距離屏幕頂部還有80。javascript
跑起來運行後發現的一個嚴重的問題是,若是Tab導航控件
的內容區域存在ScrollView或者ListView時,沒法滾動,只有最外層能夠滾動,也就是手勢滾動被攔截了?html
一開始想兩種大的思路:一種是徹底靠JS層面,經過ScrollView暴露的API去實現,第二種是原生+JS,這裏涉及到幾個關鍵的東西,如何尋找Tab導航控件
中的ScrollView
或者ListView
和控制手勢實現的效果 -- 外層滾動容器到頂部+手勢往上則通知內層滾動容器開始滾動;內層到頂部+手勢往下則通知外層開始滾動。java
發現第一種方法在解決如何尋找子控件並判斷滾動狀態上沒有方法(多是我沒發現)以及性能上的考量,那就採用第二種方法。react
爲了解決上面的問題,咱們須要瞭解幾個關鍵點。android
所以,網上搜尋這兩個問題的相關資料和解決辦法,判斷是否到底部很容易搜到了,固然瞭解了其原理。另外,判斷手勢是往上滑仍是往下滑的問題放到後面說明。git
尋找內層滾動容器,一開始是認爲遞歸尋找可見的ScrollView實例(Android中界面控件是一種樹形結構),經過Hierarchy Viewer
工具發現這三個都是可見的,隨後對比三個ScrollView屬性發現其在屏幕上的LocationOnScreenX
座標不一樣,若是當前滾動容器顯示則等於0。github
剩下最後一個如何通知內層容器滾動呢?先賣個關子,在解決這個問題以前,咱們先來了解下Android中的View事件是如何傳遞的。react-native
正所謂知己知彼,百戰不殆,看看Android觸摸事件類型有哪些?咱們想下玩手機的時候手指的狀況:落下手指,擡起手指,移動手指是三種基本的操做,其實也是3種觸摸事件,分別表明着MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE
ide
簡單來講,以下圖所示:觸摸事件發生後,若是事件的座標處於ViewGroup的管轄範圍,那麼首先調用ViewGroup的dispatchTouchEvent方法,而後其內部調用onInterceptTouchEvent()方法來判斷是否攔截該觸摸事件,若攔截該事件則調用ViewGroup的onTouchEvent()方法,不然的話,交給其子View的dispatchTouchEvent處理。
具體能夠參考我之前寫的事件分發機制學習。工具
回過頭來說外層滾動容器通知內層滾動,其實通知滾動至關於不攔截事件,那麼就是重寫 onInterceptTouchEvent
方法並返回false。而這個方法會隨着手勢不斷調用,這時候聰明的你想到了啥?根據手觸摸屏幕的y座標差來判斷手勢往上仍是往下。手指滑動時會產生一系列觸摸事件,這裏有兩種狀況:說明下屏幕的左上角是座標原點,沿着右邊是x軸,左邊則是y軸。
① Down -> Move ... -> Move -> UP
② Down -> Move ->... -> Move
記錄Down觸摸事件的Y座標值做爲起始值,Move或者UP的Y座標值做爲末尾值,二者之差大於最小滑動值則說明向上滑,小於最小滑動值則說明向上滑(這裏簡化了條件,若是是實現OnGestureListener
的話判斷滑動的話還有X軸滑動速度值和Y軸滑動速度值)。到這裏前面提的兩個問題都獲得解決了,下面開始真正上手了。
參考 RN 0.51中文文檔,咱們須要作這些東西:
3.建立實現了ReactPackage接口的類
根據前面的分析,咱們知道寫原生滾動控件主要是重寫控制攔截事件方法onInterceptTouchEvent
,這裏先說明下咱們只須要判斷當前 Tab導航控件
存在 ScrollView
的話才進入咱們的邏輯進行攔截控制,不然按默認的邏輯。
MotionEvent.ACTION_DOWN
事件中,經過前面分析的條件尋找第一個子 ScrollView
,代碼以下:private ScrollView findScrollView(ViewGroup group) { if (group != null) { for (int i = 0, j = group.getChildCount(); i < j; i++) { View child = group.getChildAt(i); if (child instanceof ScrollView) { //獲取view在整個屏幕中的座標若是x==0的話表明這個scrollview是正在顯示 int[] location = new int[2]; child.getLocationOnScreen(location); System.out.print("locationx:" + location[0] + ",locationy:" + location[1]); if (location[0] == 0) return (ScrollView) child; else continue; } else if (child instanceof ViewGroup) { ScrollView result = findScrollView((ViewGroup) child); if (result != null) return result; } } } return null; }
聲明計算滑動手勢的兩個點 Down點(x1, y1) Move點(x2, y2)
,這樣出現兩種狀況:向上滑,向下滑
在經過isAtBottom
方法,判斷RNFixScrollView是否滑到底部。
public boolean isAtBottom() { return getScrollY() == getChildAt(getChildCount() - 1).getBottom() + getPaddingBottom() - getHeight(); }
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!mScrollEnabled) { return false; } int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { //當手指按下的時候 x1 = ev.getX(); y1 = ev.getY(); scrollView = findScrollView(this); isIntercept = false; } if ((action == MotionEvent.ACTION_MOVE) || (action == MotionEvent.ACTION_UP)) { //Tab導航控件是否存在ScrollView if (scrollView != null) { //當手指移動或者擡起的時候計算其值 x2 = ev.getX(); y2 = ev.getY(); //判斷RNFixScrollView是否到底部 isbottom = isAtBottom(); //向上滑動 if (y1 - y2 > FLING_MIN_DISTANCE ) { if (!isbottom) { isIntercept = true; } else { isIntercept = false; } return isIntercept; } //向下滑動 else if (y2 - y1 > FLING_MIN_DISTANCE ) { int st = scrollView.getScrollY(); if (!isbottom) { isIntercept = true; } else { if (st == 0) { isIntercept = true; } else { isIntercept = false; } } return isIntercept; } } } //不加的話 ReactScrollView滑動不了 if (super.onInterceptTouchEvent(ev)) { NativeGestureUtil.notifyNativeGestureStarted(this, ev); ReactScrollViewHelper.emitScrollBeginDragEvent(this); mDragging = true; enableFpsListener(); return true; } return false; }
以上代碼完成了第一步建立原生固定滾動控件主要邏輯。
簡單講下,copy RN自帶的ScrollViewManager
類,修改類名和其餘引用到ScrollViewManager
。另外注意修改字段,REACT_CLASS = "RNFixedScrollView",這個與JS的模塊的名字存在映射。
RNAppViewsPackage 類
public class RNAppViewsPackage implements ReactPackage { @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); return modules; } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Arrays.<ViewManager>asList( new RNFixedScrollViewManager() ); } }
MainApplication類進行註冊
@Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new RNAppViewsPackage() ); }
簡單講下,copy RN自帶ScrollViewJS的module
,修改註釋上 providesModule
的值RNFixedScrollView
以及導出原生模塊的名稱,與第二步的值存在映射。
if (Platform.OS === 'android') { nativeOnlyProps = { nativeOnly: { sendMomentumEvents: true, } }; AndroidScrollView = requireNativeComponent( 'RNFixedScrollView', (ScrollView: React.ComponentType<any>), nativeOnlyProps ); }
完成上面的內容後,能夠經過導入 import RNFixedScrollView from './modules/RNFixedScrollView'
,使用 RNFixedScrollView
控件
爲了模擬這個界面,構建了下面的代碼,其中 ViewPagerPage
組件是Tab導航控件
,詳細代碼請轉到 github。
<View style={styles.container}> <RNFixedScrollView showsVerticalScrollIndicator={false}> <View style={{ backgroundColor: '#87cefa', height: 200, }}> </View> <ViewPagerPage style={{height: windowHeight- 80}}/> </RNFixedScrollView> </View>
FlatList
,其餘兩個則顯示文字。import {StyleSheet, View, Text, Platform, Image, TouchableOpacity, Animated, Dimensions, FlatList} from 'react-native'; import React, {Component} from 'react'; import {PagerTabIndicator, IndicatorViewPager, PagerTitleIndicator, PagerDotIndicator} from 'rn-viewpager'; const windowWidth = Dimensions.get('window').width; export default class ViewPagerPage extends Component { static title = '<FlatList>'; static description = 'Performant, scrollable list of data.'; state = { data: this.genItemData(20,0), debug: false, horizontal: false, filterText: '', fixedHeight: true, logViewable: false, virtualized: true, }; genItemData(loadNum,counts){ let items = []; for(let i=counts;i<counts+loadNum;i++){ items.push({key:i}); } return items; }; _onEndReached(){ this.setState((state) => ({ data: state.data.concat(this.genItemData(10, state.data.length)), })); }; render() { return ( <IndicatorViewPager style={[{backgroundColor: 'white', flexDirection: 'column-reverse'},this.props.style]} indicator={this._renderTitleIndicator()} > <View style={{backgroundColor: 'cornflowerblue'}}> <Text>這裏是課程介紹</Text> </View> <View style={{backgroundColor: 'cadetblue'}}> <FlatList ItemSeparatorComponent={() => <View style={{height: 1, backgroundColor: 'black', marginLeft: 0}}/>} data={this.state.data} onEndReached={this._onEndReached.bind(this)} onEndReachedThreshold={0.2} renderItem={({item}) => <View style={{ justifyContent: 'center',height:40,alignItems:'center'}}><Text style={{fontSize: 16}}>{"目錄"+item.key}</Text></View>} /> </View> <View style={{backgroundColor: '#1AA094'}}> <Text>相關課程</Text> </View> </IndicatorViewPager> ); } _renderTitleIndicator() { return <PagerTitleIndicator style={{ backgroundColor: 0x00000020, height: 48 }} trackScroll={true} itemStyle={{width: windowWidth / 3}} selectedItemStyle={{width: windowWidth / 3}} titles={['詳情介紹', '目錄', '相關課程']}/>; } }