定制一款功能强大的react-native导航组件-ANavigator

ANavigator稍微复杂点的移动应用都会有页面跳转的场景,即用户在页面A上点击某个功能,比如查看内容详情或者帮助信息,跳转到下个页面B,在用户处理完B后,通过滑动返回,或者返回按钮返(android中的返回键)回到原页面A。有页面跳转的地方就会涉及到页面栈的管理,在IOS原生应用中使用UINavigationController来管理页面栈,react-native(RN)中也提供了Navigator来管理页面栈。目前RN中提供了3中Navigator:Navigator,NavigatorIOS, NavigationExperimental。NavigatorIOS是基于IOS原生NavigationController的,一般RN代码都考虑夸平台,所以不太会用它。NavigationExperimental目前项目中还未大量使用,如果需要,以后可以定制一个基于此的ANavigator。本文主要是基于Navigator的创建定制一个通用的ANavigator,除了有基本的页面切换功能外,还希望实现以下功能:1. 便捷的控制左右按钮,title2. 统一的RN页面返回原生页面3. 处理android的返回按键4. 支持滑动返回5. 页面进入,离开的通知消息6. 可定制化的行为如导航栏,页面切换方式等## 一,初步了解Navigator1,Navigator除了push,pop外,还有几个方法贯穿Navigator的活动周期,理解这些方法对可以更好地使用Navigator完成需要实现的功能。* configureScene:Navigator render之前调用,可以用来设置页面切换的动画,滑动返回参数* renderScene:Navigator render的时候调用,在此构建新页面,可以传递参数给新页面* onDidFocus:RN页面显示的时候,不论push到新页面,还是pop回老页面,会被调用,参数是显示页面的route* 除此之外,Navigator还接受一个navigationBar参数,该NavigatorBar可以提供LeftButton,RightButton,Title三个函数设置新页面的左右按钮,以及Title,可以提供不同的参数定制NavigatorBar显示。不传的时候,不使用Navigator自带的导航栏。2,为实现上面的功能,需要扩展Navigator的功能,那问题来了,选择‘继承’还是‘组合’呢?本来打算通过继承Navigator,再在此基础上重载一些方法来定制化其功能,但后来发现行不通。主要原因:Navigator在.30版本还是通过React.CreateClass创建的,与ES6支持的extends并不能很好地兼容,继承的时候不能覆盖相应的方法,没法扩张,所以只能放弃继承。可以参考:[http://stackoverflow.com/questions/35909476/how-to-extend-react-component-class-created-via-react-createclass](http://)(You can't extend an old style component with an ES6 one, at least not with the ES6 extends syntax)。所以只能新建一个类,在内部‘组合使用’Navigator类,新建的ANavigator大致这样:```import Navigator from ‘Navigator';class ANavigator extends Component {reder() {…return (}```在此情况下如何扩张Navigator的功能呢?覆盖Navigator的方法在使用到Navigator的时候,判断对应的方法是否被重载过,如果没有重载这些方法。可以放在每次push, pop页面的时候,renderScene被调用。改进后的renderScene如下:```renderScene(route, navigator) {…if(navigator.overrideMethod)return;let {pop} = navigator;navigator.pop = function(route){…pop.apply(this,arguments);…};navigator.overrideMethod = true;…}```3,组件与route    组件是什么,route又是什么,两者的关系如何?    Navigator中的组件是负责页面功能的对象,是真正承担显示,逻辑工作的对象。route是用来给Navigator管理组件的映射,可能会带一些参数,用来控制组件的具体功能。两者没有直接的关系。为了后续的一些功能,需要建立两者的联系。从上面讨论的Navigator生命周期函数可以看到,renderScene是创建组件的地方,而且有route信息,可以在此建立两者关系。renderScene创建组件的时候把route传给组件```renderScene(route, navigator) {…let Screen = route.screen;route.props = route.props || {};return ();}```这样在组件内部可以将组件的this传给route,这样route就可以通过这个对象访问组件的所有参数,方法了。```constructor(props) {...props.route.component = this;...}```组件访问route的方法:props.routeroute访问组件中变量,方法:route.component.***## 二,实现预定的功能准备工作差不多了,下面考虑如何实现预定的几个功能。1)便捷的控制左右按钮,titlenavigatorBar在定义Navigator的时候就要提供,如果把导航栏显示逻辑都放在这里而具体显示的内容又要根据不同的组件有所不同,则navigatorBar就成了一个耦合点,关联了所有页面,不利于解耦。为了能够便捷地控制左右按钮等信息,希望把设置内容的逻辑放到当前显示的组件中,在Navigator的navigatorBar中仅仅读取这些信息并显示,而前面的准备工作3中关联了route和组件,因此可以在组件中设置信息供以后在route中使用。在组件中可以这样定制左按钮:```constructor(props) {…this.leftButton = this.leftButton.bind(this);props.route.leftView = this.leftButton;…}```具体的设置逻辑在leftButton中:leftButton(route, navigator) { …}navigationBar的leftButton的实现如下:const NavigationBarRouteMapper = {    // 左边Button    LeftButton(route, navigator, index) {        let leftView;        if (route.leftView) {            leftView = route.leftView(route, navigator);            return leftView;        }….}这样的话可以在组件中定义左右按钮,title等,navigationBar仅仅负责调用各自的方法。组件之间,组件与Navigator相互独立,navigator功能单一,便于维护。2)从RN页面退回到原生页面统一到pop中处理,如果pop的时候发现当前是最后一页,Android调用BackAndroid.exitApp(),IOS调用原生的pop。```navigator.pop = function(route){…let length = navigator.getCurrentRoutes().length;if(length>1){pop.apply(this,arguments);} else {if (Platform.OS === 'android') {BackAndroid.exitApp();} else if (Platform.OS === 'ios') {UIManagerNativeModule.popViewControllerAnimated(animated);}}};```3)处理android的返回按键接受hardwareBackPress事件,转化为Navigator的pop操作,实际的控制逻辑放在pop中处理,包括判断是否需要返回原生页面。```BackAndroid.addEventListener('hardwareBackPress', handleBackEvent);handleBackEvent() {…navigator.pop();…}```4)支持滑动返回根据当前RN页面在Navigator栈中位置,滑动返回分两种情况处理:* 第一个RN页面,即Navigator的根页面,这时没法通过Navigator返回,可以通过原生层支持滑动返回* 不是第一个RN页面,可以通过控制RN的Navigator的滑动返回,为了原生滑动不影响RN返回,需要禁止原生层的滑动返回这里主要介绍RN中滑动返回的控制,原生的滑动返回就不详细介绍了,由于苹果默认的滑动返回行为限制,我们使用的是FDFullscreenPopGesture控制原生的滑动返回。配置RN的滑动返回Navigator不仅可以配置push,pop的动画,也可以配置滑动返回的参数。具体配置是在Navigator的SceneConfigs中,它提供了多种页面切换的方式,如PushFromRight, HorizontalSwipeJump等。在IOS中常用的PushFromRight,新页面会从右侧进入,它使用的pop手势默认参数值如下为:```var BaseLeftToRightGesture = {…// Region that can trigger swipe. iOS default is 30px from the left edge  edgeHitWidth: 30,…direction: 'left-to-right',};```可以通过设置edgeHitWidth控制是否可以滑动返回。如果设置为0,则表示禁止滑动返回,如果设置屏幕宽度,则相当于支持全屏幕滑动返回手势。可以通过一个[0,1]值来配置支持的滑动范围。```configLeftToRightPopRange (range) {let width = 0;if (range >= 0 && range <= 1) {width = range * kScreenWidth;}const pushFromRight = Navigator.SceneConfigs.PushFromRight;pushFromRight.gestures.pop.edgeHitWidth = width;return pushFromRight;}```enable/disable原生滑动返回功能根据navigator中管理的route数判断是否是第一个RN页面```handleLeftPanGesture(navigator) {const routes = navigator.getCurrentRoutes();const len = routes.length;// open native LefToRight GestureUIManagerNativeModule.configNativeLeftToRightGesture(findNodeHandle(navigator),len === 1);}```5)页面进入,离开的通知消息页面的进入离开根据参与的页面是RN的还是原生的可以分* RN页面间的页面间的切换* RN与原生页面间的切换RN页面间的进入离开不论是第一次显示,还是push,pop,所有页面显示的时候都会调起onDidFocus事件,onDidFocus的参数是正在显示的新页面的route,因此可以在此作页面进入,离开(进入新页面就意味着离开老页面)通知。```_onDidFocus(route) {…    this.pageOut();    this.pageIn(pageName);    …}```RN与原生页面间的切换除了纯RN页面间的切换外,实际产品中往往会有RN管理的页面与原生的页面发生交互,大致有两种情况* 在RN A页面切换到原生B页面* 在原生B页面完成操作回到页面A(原生页面打开RN页面,也算交互之一,在RN第一次显示中解决了,不需要这里再处理了。)这两种情况发生时,RN的Navigator不会有任何通知发出,所以仅仅依靠RN代码没法做页面进入/离开的处理工作。必须借助原生代码通知RN页面。其实任何一个RN页面都是通过原生ViewController加载的,只不过View是通过RN的RootView提供的。在这两种页面切换情况发生时,RN所在的原生页面都会发生viewWillAppear,viewWillDisappear,会被正确调起,可以在对应的地方发送通知给RN做对应的处理工作。考虑到可能有多个Navigator同时存在(比如一个TabController容器中有多个RN Navgator管理各自页面),所以需要一个标志区分各个Navigator,在页面显示/隐藏时通过原生VC传给RN,RN的Navigator根据这个标志判断自己是否需要处理该通知。具体做法可以从原生代码生成一个UUID,在创建RN页面的时候传入,最后由Navigator保存。如果该VC是第一次打开,这是RN初次显示,如果不是第一次显示,则是原生页面返回,RN页面显示,应该通知对应的Navigator,向其管理的顶级页面发送顶级页面进入通知```- (void)viewWillAppear:(BOOL)animated {…if (!self.firstTimeLoad) {[((RCTRootView *)self.view).bridge.eventDispatcher sendAppEventWithName:@"_pop_" body:@{@"tag":self.tag}];}…}```当该VC不管因为何种原因disappear时,应该通知对应的Navigator,向其管理的顶级页面发送页面离开通知```- (void)viewWillDisappear:(BOOL)animated {…[((RCTRootView *)self.view).bridge.eventDispatchersendAppEventWithName:@"_push_" body:@{@"tag":self.tag}];…}```6)可定制化的行为如导航栏,页面切换方式等另外。为了提供一定的定制化功能,Navigator接受外部的renderScene,  configureScene,navigationBar,如果有外部传入值的话,使用外部的值所以改进后的ANavigator是这样的:```class ANavigator extends Component {reder() {…return (}  _configureScene(route) {const customizedConfig = route.SceneConfigs || this.props.configureScene;if (customizedConfig) {return customizedConfig(route);}return this.configureScene();}configureScene() {…}_renderScene(route, navigator) {this.navigator = navigator;return this.renderScene(route, navigator);}renderScene(route, navigator) {if (this.props.renderScene) {return this.props.renderScene(route, navigator);}…}_onDidFocus(route) {if (this.props.onDidFocus) {this.props.onDidFocus();}…}```至此,ANavigator实现完成,除了完成页面跳转,配合原生代码还实行了前文所列的几个常用功能,功能完备,便于维护,使用方便。如果还有别的功能可以通过route传入,甚至继承ANavigator覆盖其方法。sample代码见:[https://github.com/PingAnYee/ANavigator](http://)参考:[http://bbs.reactnative.cn/topic/20/新手理解navigator的教程](http://)[https://facebook.github.io/react-native/releases/next/docs/using-navigators.html](http://)[https://facebook.github.io/react-native/releases/next/docs/navigation.html](http://)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习 本人...
    珍此良辰阅读 7,230评论 33 15
  • React Native 进阶(四)--导航器 导航器对比 如果你刚开始接触,那么直接选择Navigator就好。...
    呼呼哥阅读 983评论 0 0
  • 一、Navigator 1、简单介绍:大多数时候我们都需要导航器来应对不同场景(页面)间的切换。它通过路由对象来分...
    关关雎鸠1206阅读 682评论 0 0
  • 看着大家都慢生活,重养生,自在滋润,才发现自己真是琐事缠身。看来与返老还童无缘了,呵呵!但也没关系,一件件处理吧,...
    王悦yue阅读 164评论 0 4
  • 休息了一会李光头拿着棍子怒目圆睁气势汹汹嘴里啊啊啊乱叫的朝两只狗冲去,只顾着跑了没看路,脚被路上的砖头一绊在距离狗...
    52荷兹阅读 272评论 0 1