说道这里,我尝试着写了个测试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={['详情介绍', '目录', '相关课程']}/>; } }