|
Flutter Navigator局部页面切换实践
Flutter Navigator局部页面切换实践
陈维@贝壳找房
贝壳产品技术
贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容
2021年11月25日 20:52
01背景在移动端,经常会存在局部视图切换的场景,比如图1所示的场景, 页面的左边部分支持切换到下一个页面并且下一个页面可以返回到上一个页面。在Android中,可以使用Fragment实现图中所示的效果, iOS中可以使用UINavigationController实现类似的效果。那么在Flutter中,应该怎么实现这种局部页面切换呢?图1 局部页面切换示意图在Flutter中,页面切换是通过Navigator实现的, 那么能否考虑将Navigator用于局部页面切换呢?答案是肯定的。本文将重点介绍使用Navigator进行局部页面切换,以及在使用的过程中碰到的一些问题和解决方案。由于业务中目前使用的Flutter版本为v1.12.13,所以本文也会基于这个版本进行分析。02Navigator简介2.1 Navigator源码浅析Navigator是 Flutter中的导航组件,用于管理页面的跳转和返回。打开Navigator源码,可以看到部分源码如下:从源码可知:●Navigator 是一个StatefulWidget,其对应的State为NavigatorState,这意味着Navigator可以直接作为Widget使用在Row、Column、Container等组件里;●NavigatorState中build返回的是一个Overlay组件,Overlay组件里初始化了一个OverlayEntry的列表,OverlayEntry的builder方法就可以返回需要显示的页面;●查看Overlay的源码可以发现,Overlay维护了一个栈(Stack)来显示这些OverlayEntry,所以Navigator实质上就是使用了一个栈来管理页面;●打开新页面时调用的Navigator.push实质上就是一个入栈操作,退出页面时的Navigator.pop实质上就是一个出栈操作。2.2 Navigator初始化从2.1节可知,Navigator 是一个StatefulWidget,正常情形下,StatefulWidget是在build里面使用的,但是,我们在平时开发页面的过程中,build里面是没有使用到Navigator的,那么它是在哪里使用的呢?打开Flutter DevTools,查看Widget树可以发现,App下有一个Widget名为MaterialApp,它里面返回了一个Navigator,如图2所示:图2 Widget树示意图查看MaterialApp源码,可以发现在它的build里面创建了WidgetsApp,在WidgetsApp的build方法里面创建了Navigator,部分代码如下:所以Navigator是使用在MaterialApp的build方法里的,而MaterialApp是在app初始化调用runApp方法时创建的Widget中返回的,所以在App初始化的时候,就初始化了一个Navigator组件,用于管理Flutter页面。综上,可以得出如图3所示的Flutter app结构图:图3 Flutter app结构示意图03使用Navigator进行局部页面切换根据前文可知,Navigator 是一个StatefulWidget,所以可以直接使用Navigator组件进行局部页面切换。以图1这种页面为例,页面是一个左右排列结构,页面的左边部分存在局部页面切换,那么可以返回一个Row组件,左边使用Navigator,右边返回其他widget。页面结构图如图4所示:图4 图1对应页面结构图图中,MainPage表示的是整个主页面,左边是需要切换的页面,使用Navigator组件,它的子页面使用SubPage表示,SubPage1表示第一个子页面,SubPage2表示第二个子页面。整个页面对应的示例代码如下:上述代码中,Navigator里面添加了子页面1,如果需要由子页面1切换到子页面2,只需要在子页面1里面调用Navigator.push即可:从第二个子页面返回第一个子页面, 则可以直接使用Navigator.pop(context, result)。04问题锦集了解Navigator的使用方式之后,就可以在业务中正常使用Navigator了。在我们的业务中,涉及到页面切换的场景除了图1所示的之外,还有弹窗(Dialog)中切换页面(如图5所示),抽屉(Drawer)中切换页面等,在使用Navigator进行局部页面切换的过程中,我们也碰到了各种各样的问题。下文中,我们会以弹窗(Dialog)场景中切换页面为例来将介绍一下这些问题、问题出现的原因以及解决方法。4.1 动画问题碰到的第一个问题就是切换页面时的动画问题。一般而言,Flutter页面切换时,新的页面会从屏幕的右端或者底部平移出来,正常情形下,这样是完全没问题的。但当我们使用Navigator组件时,我们的视图可能在屏幕中的任何位置,此时,新页面再从右端或者底部平移出来,体验可能就不太友好了。如图5所示,页面切换时,新页面从屏幕底部出来,而不是弹窗的底部出来,在视觉上屏幕底部出现了闪烁:图5 新页面从屏幕底部进入示意图这个问题是由MaterialPageRoute导致的,它封装了使用了Material主题的动画,在Navigator.push时使用MaterialPageRoute就会出现图5所示的动画效果。可以使用PageRouteBuilder或者自定义Route来替换MaterialPageRoute,然后自定义页面切换的动画效果来避免这个问题。动画效果可以使用Flutter官方的动画效果,比如FadeTransition可以实现alpha切换效果、ScaleTransition可以实现缩放效果、SlideTransition可以实现平移效果等。下面是一个使用PageRouteBuilder结合FadeTransition动画实现alpha切换的示例:除了alpha切换,在我们的应用中,从屏幕左侧进入或者底部进入也是比较常见的场景。正常情形下,这种平移动画可以使用SlideTransition,但是当我们在Navigator push时使用SlideTransition后发现实现的效果会和图5类似,新的页面会从Navigator组件之外平移进Navigator里,这种效果显然是不理想的,需要考虑使用别的方案来替代SlideTransition。实践发现,如果新的页面沿着x轴或者y轴做缩放动画,视觉效果和平移是比较类似的,所以可以考虑使用缩放动画来替代平移动画。官方提供的ScaleTransition会同时改变x的值和y的值,而我们只需要沿着x轴或者y轴做缩放动画,因此需要对ScaleTransition做一些修改,固定x或者y的值。以y方向的缩放动画为例,我们定义ScaleYTransition,在build方法里固定x的值,就可以实现y方向的缩放动画了,下面是代码示例(其他代码与ScaleTransition一致):参考上面调用FadeTransition的方式,我们调用ScaleYTransition,就可以实现从底部切入的效果了,如图6所示:图6 页面切换底部进入效果图同理,固定y值,就可以实现水平方向的缩放动画,具体效果可以参考图1。4.2 pop/push问题解决了动画问题之后,使用Navigator就可以正常进行局部页面的切换了。当我们进入页面之后点击弹窗的关闭按钮时,出现了一个新问题,具体现象为:点击关闭之后弹窗没有消失,但是弹窗内容变成了空白页面(如图7所示)。其中关闭弹窗调用的方法是Navigator.pop(context)。图7 关闭弹窗时出现出现空白页面示意图查看Navigator.pop(context)源码,发现它调用方法Navigator.of(context).pop(),查看Navigator.of(context)这个方法,源码如下:这里面有个重要的参数rootNavigator:●当rootNavigator的值为true时,会执行方法context.findRootAncestorStateOfType(),此时会递归查找到最顶层的NavigatorState;●当rootNavigator的值为false时,会执行方法context.findAncestorStateOfType(),会查找到最近的NavigatorState。根据前文可知,此时,Flutter app中存在两个Navigator,第一个Navigator为WidgetsApp中创建的Navigator,它用于管理主页面,第二个 Navigator为局部页面切换时使用Navigator,它用于管理子页面。此时,app结构图可以可以简化为如图8所示的结构:图8 页面中使用Navigator时app结构图对照Navigator.of(context)这个方法和图8所示的结构图可知,在子页面里调用Navigator.of(context)这个方法时,如果rootNavigator为false,就会查找到最近一个Navigator,也就是局部页面切换所使用的Navigator(下文中,将称该Navigator为子Navigator)。当rootNavigator传为true时,会递归查找到最顶层的Navigator,即WidgetsApp里的Navigator(下文中,将称该Navigator为RootNavigator)。在子页面里调用Navigator.pop(context)方法时,rootNavigator的值默认为false,此时会查找到子Navigator并调用它的pop方法,由于当前子Navigator只有一个子页面,pop之后,就没有别的子页面了,而我们也没有设置默认子页面,所以就出现了空白。如果想要关闭整个弹窗,则需要调用RootNavigator的pop方法,此时需要将rootNavigator传为true。不仅仅只有弹窗中点击会出现空白现象,在页面中使用Navigator时,也会出现类似的现象, 在子页面里关闭主页面时也需要设置rootNavigator为true。push和pop一样,也存在rootNavigator传参问题。在子页面调用Navigator.push时,如果rootNavigator的值为false,那么新的页面就会是子Navigator的子页面。如果rootNavigator的值为true,就会调用RootNavigator的push方法,会跳转到一个和主页面同级的新页面。总结一下:在子页面里调用Navigator.push或者Navigator.pop时,需要设置rootNavigator参数来明确使用哪个Navigator。如果需要处理主页面的跳转和回退,那么就需要使用WidgetsApp里的navigator,此时需要将rootNavigator传为rue;如果需要处理子页面的跳转和回退,rootNavigator可以不传或者传为false。4.3 子页面不响应返回键在Flutter开发中,由于Android设备存在返回键,所以需要考虑Android设备的返回事件,通常使用WillPopScope来处理返回事件。我们发现,在使用Navigator进行子页面切换时,子页面不会响应返回事件,具体现象为:在Android设备上,进入主页面,子页面1使用Navigator.push跳转到子页面2之后按下系统的返回键时,不会由子页面2返回到子页面1,反而是主页面退出了,效果如图9所示(设备使用的是华为Matepad,系统导航方式使用的是手势导航,从屏幕右边滑动时执行返回操作):图9 Android返回问题示意图从现象来看,系统调用了Navigator的pop方法,但是调用的却不是子Navigator的pop方法,而是RootNavigator的pop方法。在4.2节中,我们通过添加rootNavigator参数可以指定调用哪个Navigator,在这个地方是否可以参照这个方法让子Navigator响应返回事件呢?在Navigator的pop方法里断点,可正常捕获断点,得到按下返回时调用链路为:查看链路中的方法,发现决定调用哪个Navigator的方法为_WidgetsAppState里的didPopRoute(),方法部分代码如下:查看源码可知,调用maybePop的navigator由WidgetsApp initState时创建,同时,只在WidgetsApp调用didUpdateWidget才会更新。所以,目前来看,没有办法通过传参来指定Navigator。那么,子页面如何正常响应系统返回事件呢?可以考虑在主页面里面处理子页面的返回事件,既然主页面可以正常接收返回事件,那么只需要在主页面里重写WillPopScope方法,在onWillPop处理子页面的返回事件即可。具体来说,给子Navigator设置key,在主页面的onWillPop里面通过该key值获取子Navigator,如果子Navigator.canPop()为true,那么就执行NavigatorState.maybePop方法,具体流程图如图10所示:图10 子页面处理返回事件流程图具体示例代码如下:参照这种方法,我们实现的最终效果如图11所示,按返回时可以正常返回到上一级页面了。图11 子页面响应返回事件效果图05总结本文详细介绍了使用Navigator进行局部页面切换的方法,使用过程中碰到的一些问题以及解决方法。目前Flutter已发布了2.5版本,我们也正在积极适配,适配的过程中发现,尽管源码出现了一些变动,但是这些问题在2.5版本上依旧存在,解决方法也依旧通用。大家如果在新的版本上碰到了这种问题,也可以参考文中的解决方法。
预览时标签不可点
Flutter15移动端37大前端69Flutter · 目录#Flutter上一篇FlutterEngine在Pad上的演变下一篇Flutter For Web多端一体化开发和原理分析关闭更多小程序广告搜索「undefined」网络结果
|
|