說道這裏,我嘗試着寫了個測試js例子,最外面套一個ReactNative自帶的ScrollView
並設置視頻播放控件
的高度爲200
和 Tab導航控件
的style={{height: windowHeight- 80}}
,那這樣滾動距離到120時,滾動條到底部了,視頻播發控件的區域距離屏幕頂部還有80。html
跑起來運行後發現的一個嚴重的問題是,若是Tab導航控件
的內容區域存在ScrollView或者ListView時,沒法滾動,只有最外層能夠滾動,也就是手勢滾動被攔截了?react
一開始想兩種大的思路:一種是徹底靠JS層面,經過ScrollView暴露的API去實現,第二種是原生+JS,這裏涉及到幾個關鍵的東西,如何尋找Tab導航控件
中的ScrollView
或者ListView
和控制手勢實現的效果 -- 外層滾動容器到頂部+手勢往上則通知內層滾動容器開始滾動;內層到頂部+手勢往下則通知外層開始滾動。android
發現第一種方法在解決如何尋找子控件並判斷滾動狀態上沒有方法(多是我沒發現)以及性能上的考量,那就採用第二種方法。git
爲了解決上面的問題,咱們須要瞭解幾個關鍵點。github
所以,網上搜尋這兩個問題的相關資料和解決辦法,判斷是否到底部很容易搜到了,固然瞭解了其原理。另外,判斷手勢是往上滑仍是往下滑的問題放到後面說明。react-native
尋找內層滾動容器,一開始是認爲遞歸尋找可見的ScrollView實例(Android中界面控件是一種樹形結構),經過Hierarchy Viewer
工具發現這三個都是可見的,隨後對比三個ScrollView屬性發現其在屏幕上的LocationOnScreenX
座標不一樣,若是當前滾動容器顯示則等於0。bash
剩下最後一個如何通知內層容器滾動呢?先賣個關子,在解決這個問題以前,咱們先來了解下Android中的View事件是如何傳遞的。ide
正所謂知己知彼,百戰不殆,看看Android觸摸事件類型有哪些?咱們想下玩手機的時候手指的狀況:落下手指,擡起手指,移動手指是三種基本的操做,其實也是3種觸摸事件,分別表明着MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE
工具
簡單來講,以下圖所示:觸摸事件發生後,若是事件的座標處於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軸滑動速度值)。到這裏前面提的兩個問題都獲得解決了,下面開始真正上手了。
######1.建立原生固定滾動控件
根據前面的分析,咱們知道寫原生滾動控件主要是重寫控制攔截事件方法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();
}
複製代碼
1.RNFixScrolView已到底部&&向上滑:不攔截
2.RNFixScrolView未到底部&&向上滑:攔截
3.RNFixScrolView未到底部&&向下滑&&子ScrollView已到頂部:攔截
4.RNFixScrolView已到底部&&向下滑&&子ScrollView未到頂部:不攔截,
@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={['詳情介紹', '目錄', '相關課程']}/>;
}
}
複製代碼