写点什么

普普通通的 Route

作者:最强上单
  • 2021 年 12 月 07 日
  • 本文字数:9530 字

    阅读完需:约 31 分钟

普普通通的Route

在介绍 Navagator 之前,我们先介绍一个前置——Route。Route 出现在我们代码中的各个位置,比如我们打开的页面,放到了 MaterialRoute 中,我们的弹窗放到了_DialogRoute 中,我们的底部半屏 Sheet 放到了_ModalBottomSheetRoute 中。下面我们窥探一下普普通通的 Route 中的小秘密。

Route 是什么

我们先看看官方是怎么定义 Route 的。


An abstraction for an entry managed by a [Navigator]被Navigator管理的抽象的实体  This class defines an abstract interface between the navigator and the"routes" that are pushed on and popped off the navigator. Most routes havevisual affordances, which they place in the navigators [Overlay] using oneor more [OverlayEntry] objects.这个类定义了一些抽象的借口,可以让navigator去pop或者push。大多数route都是可视的,因为route会被放置在Overlay中。  
复制代码


从上面的描述来看,Route 就是一个被 Navigator 管理的类,由于 Overlay 中的存在,Route 是可使的。


从描述似乎看不出什么,我们看他封装的接口。

属性

Route 的属性很少,只有两个 overlayEntries:List<OverlayEntry>navigator:NavigatorState。overlayEntries 是 OverlayEntry 的数组,当 Route 被 push 进来之后,overlayEntries 会被添加到 Overlay 中,因此 Route 的也就显示到页面上了。并且 Overlay 就是 navigator 的 build 方法中构建的。


说到这里,我们就知道了,我们的页面也好,对话框也好,都是显示在 Overlay 中的

方法

Route 中定义了一组基本的接口,供 Navigator 实现管理的功能:初始化、打开、关闭、改变、判断等等。

void install(OverlayEntry insertionPoint)

该方法是 Route 的初始化方法,当 Route 被压入 navigator 中时(push),就会先调用该方法进行初始化。

把 Route 的 overlayEntries 添加到 Overlay 中,插入的位置就是 insertionPoint。

不同的子类,会添加各自的内容。比如动画效果等等。

TickerFuture didPush()

  TickerFuture didPush() {    return TickerFuture.complete()..then<void>((void _) {      //重新获得焦点      navigator.focusScopeNode.requestFocus();    });  }
复制代码


Route 被 push 之后,会先调用 install 初始化,然后就会调用该方法。

void didReplace(Route<dynamic> oldRoute)

Route 被 replace 之后,也会先调用 install 初始化,然后就会调用该方法。

Future<RoutePopDisposition> willPop()

Navigator 的 maybePop 就会调用到这里,用来判断是否去关闭 Route。

一般情况下,栈中只剩下一个 Route 的时候,是不会调用 Pop,因为那样会出现黑屏。

bool didPop(T result)

Pop Route 的请求发出时,就会调用该方法。


该方法的返回值至关重要。因为保证了动画的执行与否。如果方法返回 true,navigator 会从历史列表移除 route,但是 dispose 没有调用。Route 需要调用 NavigatorState.finalizeRoute 方法,该方法会调用 dispose 方法。这样就保证了退出动画的执行。因为 dispose 中释放了动画控制器。

void didComplete(T result)

计算 pop 的异步结果

void didPopNext(Route<dynamic> nextRoute)

参数中的 nextRoute 被 pop

void didChangeNext(Route<dynamic> nextRoute)

route 的下一个 route 指定为 nextRoute。无论什么原因,只要 route 的 next route 发生了改变,这个方法都要被调用。

void didChangePrevious(Route<dynamic> previousRoute)

Route 的前一个 Route 被指定为 previousRoute,无论什么原因,只要 Route 的前一个 Route 发生改变,这个方法都要被调用。

void changedInternalState()

Route 的内部状态发生变化时,就会调用该方法。


willHandlePopInternally、didPop、offstage、其他的内部值变化时,都会调用该方法。

比如 ModalRoute 使用这个方法来通知子节点信息发生改变。

void changedExternalState()

Navigator 重新构建时,那就表明 Route 可能也需要重新构建。比如 MaterialApp 发生重新构建的时候。

这样就保证了 Route 所依赖的构建了 MaterialApp 的 Widget 的状态发生时 就会收到通知。

void dispose()

Route 会移除 overlays,并且释放其他的资源。不再持有 navigator 的引用

小结

现在我们知道了 Route 是什么,被 Navagator 用来管理页面。内部持有的 overlayEntry

会被插入到 Overlay 中,实现了页面叠加。下面我们看 Route 的关系网。

Route 关系网

上面介绍的是最顶层的 Route 类,定义了一些抽象的接口,下面我看一下 Route 的体系网。


上面就是 Route 的体系网。Route 及其子类持有多个 OverlayEntry,每一个 OverlayEntry 都会显示到页面上,比如弹窗的黑色遮罩是一个 overlayEntry,显示的弹窗内容又是一个 overlayEntry。还持有 NavigatorState,这样就可以访问 NavigatorState 的属性和方法。比如焦点等等。


除此之外,Route 是有继承体系的,每一层实现特定的功能。OverlayRoute 实现了将 OverlayEntry 插入到 Overlay 的功能。TransitionRoute 实现了 Route 的动画切换功能,我们看到它持有动画对象和动画控制器对象。ModalRoute 相对比较完整了,同时做了拦截,这个拦截就是我们常用的 WillPopScope 组件。在 ModalRoute 的基础上分为两类:显示弹窗、popWindow 类型的 PopupRoute,显示页面的 PageRoute。


下面我们通过初始化和释放来看,不同层级下添加的各自的功能。

Route 层级的线性初始化

先看初始化方法 install 是怎么一步步具化的。

Route.install

void install(OverlayEntry insertionPoint) { }
复制代码


Route 中只是声明了该方法,并没有具体的实现。

OverlayRoute.install

@overridevoid install(OverlayEntry insertionPoint) {  _overlayEntries.addAll(createOverlayEntries());  navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);  super.install(insertionPoint);}
复制代码


第一步:Route 的 overlayEntries 中添加了本 Route 的所有的 OverlayEntry,可能是一个、可能是多个。

⚠️注意 createOverlayEntries 是抽象方法,后面我们就知道了 createOverlayEntries 返回的都是要添加或者显示的。


第二步:navigator 的 overlay 中插入了创造出来的所有 OverlayEntry

这也就是为什么 Route 要持有 navigator 的引用。

TransitionRoute.install

@overridevoid install(OverlayEntry insertionPoint) {  _controller = createAnimationController();  _animation = createAnimation();  super.install(insertionPoint);}
复制代码


第一步:创造了动画控制器。默认就是一个动画时长是 transitionDuration 的动画控制器

默认创建的流程如下:

AnimationController createAnimationController() {  final Duration duration = transitionDuration;  return AnimationController(    duration: duration,    debugLabel: debugLabel,    vsync: navigator,  );}
复制代码


当然子类也可以有自己的实现,去做不同的控制。后面我们在看。


第二步:创建了动画对象。动画对象是当前动画控制器的动画进度

因此:TransitionRoute 主要是创建了动画对象(当前动画的进度)和动画控制器。

ModalRoute.install

@overridevoid install(OverlayEntry insertionPoint) {  super.install(insertionPoint);  _animationProxy = ProxyAnimation(super.animation);  _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);}
复制代码


ModalRoute 生成了两个代理动画。

代理动画:接收一个 Animation<double> 类作为其父级,并仅转发该父级的当前状态。

就是说:_animationProxy 的动画 value 就是 animation 的 value

这里讲一下为什么生成了两个动画:因为前一个页面需要退出,后一个页面需要进入


_animationProxy:负责当前 Route push pop 动画,和驱动前一个 Route 的动画,比如驱动前一个的 pop 动画。

_secondaryAnimationProxy:负责放到这个 Route 之上的 Route 的动画,这个动画可以让 Route 本身和新 Route 的进入和退出动画相互衔接。


这就是 install 方法,OverlayRoute.install 方法还有一个 createOverlayEntries 抽象方法,我们看看在 ModalRoute 的具化。

@overrideIterable<OverlayEntry> createOverlayEntries() sync* {  yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);  yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);}
复制代码


这里用了一个 Dart 语法:sync*和 yield

sync*和yield是Dart的一组语法关键字,sync*放在方法签名上,表明该方法return 一个Iterable数组对象。数组的元素是每一行yield生成的。
复制代码


所以上面 createOverlayEntries 方法放回的数组中,包含了两个元素**_modalBarrier 和 OverlayEntry**。


这里给大家讲一下:maintainState 字段,它表明 Route 的状态是否需要维持。

我们知道页面是叠加的,如果一个当前 Route 没有显示,被顶层的 Route 盖住了,那么它是否还需要存活在内存中呢?

这就是这个字段的含义。如果是 true,Route 就会被保存,那么下一个 Route 的一些 Future 结果,当前的 Route 就可以被正常的计算,比如刷新等等。如果设置为 false,那么在内存紧张的时候,就会回收掉。


这个字段只能被赋值一次,我们熟知的 MaterialPageRoute 就是 true。


下面,我们分别看添加的内容是什么

_buildModalBarrier

Widget _buildModalBarrier(BuildContext context) {  Widget barrier;  if (barrierColor != null && !offstage) {     final Animation<Color> color = animation.drive(      ColorTween(        begin: _kTransparent,        end: barrierColor, // changedInternalState is called if this updates      ).chain(_easeCurveTween),    );    barrier = AnimatedModalBarrier(      color: color,      dismissible: barrierDismissible, // changedInternalState is called if this updates      semanticsLabel: barrierLabel, // changedInternalState is called if this updates      barrierSemanticsDismissible: semanticsDismissible,    );  } else {    barrier = ModalBarrier(      dismissible: barrierDismissible, // changedInternalState is called if this updates      semanticsLabel: barrierLabel, // changedInternalState is called if this updates      barrierSemanticsDismissible: semanticsDismissible,    );  }  if (_filter != null) {    barrier = BackdropFilter(      filter: _filter,      child: barrier,    );  }  return IgnorePointer(    ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates              animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture    child: barrier,  );}
复制代码


上面的核心就是 ModalBarrier。也就是说 Route 里面第一个是 ModalBarrier。

barrierColor 就是我们经常在对话框中看到的遮罩色。

barrierDismissible 就是我们点击遮罩是否消失弹窗。


ModalBarrier 的 build 如下:

BlockSemantics(  child: ExcludeSemantics(    excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,    child: _ModalBarrierGestureDetector(      onDismiss: () {        if (dismissible)          Navigator.maybePop(context);//第一处      },      child: Semantics(        label: semanticsDismissible ? semanticsLabel : null,        textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,        child: MouseRegion(          opaque: true,          child: ConstrainedBox(            constraints: const BoxConstraints.expand(),            child: color == null ? null : DecoratedBox(              decoration: BoxDecoration(                color: color,              ),            ),          ),        ),      ),    ),  ),)
复制代码


注意看第一处,这就是为啥点击遮罩会消失,直接调用了 maybePop 操作。

其实加这一层,主要是为了阻止手势传递给下一层的 Route。

_buildModalScope

Widget _modalScopeCache;
Widget _buildModalScope(BuildContext context) { return _modalScopeCache ??= _ModalScope<T>( key: _scopeKey, route: this, );}
复制代码


_buildModalScope 构建的就是 __ModalScope,并指定了一个成员变量 key,而不是临时变量 key。这样就可以复用 Element 了,从而减少了重复构建。


我们在看_ModalScope。 _ModalScope 是 StatefulWidget,我们可以用 StatefulWidget 去理解它。

先看 initState 方法


@overridevoid initState() {  super.initState();  final List<Listenable> animations = <Listenable>[    if (widget.route.animation != null) widget.route.animation,    if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation,  ];  _listenable = Listenable.merge(animations);//第一处  if (widget.route.isCurrent) {    widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);//第二处  }}
复制代码


第一处:根据 animation 和 secondaryAnimation,建立起 监听器。

merge 的意思是:即会响应 animation 的变化,也会响应 secondaryAnimation 的变化。


第二处:焦点作用域 移到 当前的范围。

关于 focusScopeNode 大家可以看这一篇文章:说说Flutter中的无名英雄 —— Focus


我们在看 build 方法,这是重头戏。


@overrideWidget build(BuildContext context) {  return _ModalScopeStatus(    ...    child: Offstage(      offstage: widget.route.offstage,       child: PageStorage(        bucket: widget.route._storageBucket,         child: FocusScope(          node: focusScopeNode,           child: RepaintBoundary(            child: AnimatedBuilder(              animation: _listenable,               builder: (BuildContext context, Widget child) {                return widget.route.buildTransitions(                  context,                  widget.route.animation,                  widget.route.secondaryAnimation,                  AnimatedBuilder(                    animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),                    builder: (BuildContext context, Widget child) {                      final bool ignoreEvents = widget.route.animation?.status == AnimationStatus.reverse ||                          (widget.route.navigator?.userGestureInProgress ?? false);                      focusScopeNode.canRequestFocus = !ignoreEvents;                      return IgnorePointer(                        ignoring: ignoreEvents,                        child: child,                      );                    },                    child: child,                  ),                );              },              child: _page ??= RepaintBoundary(                key: widget.route._subtreeKey,                 child: Builder(                  builder: (BuildContext context) {                    return widget.route.buildPage(                      context,                      widget.route.animation,                      widget.route.secondaryAnimation,                    );                  },                ),              ),            ),          ),        ),      ),    ),  );}
复制代码


我们一层一层来看。


第一层:_ModalScopeStatus。是一个普通的 InheritedWidget,用来向下传递 ModalScope 的状态(Route 是谁,是不是顶层 Route,是否可以 Pop)。


第二层:Offstage。当前是否不在“舞台”上,不在舞台上的 不绘制。也就是说 Route 的 offstage 属性是 true 的时候,Route 不绘制。一般的 Route 都是 false。


第三层PageStorage。为页面中的元素,提供一个数据存放的桶。子节点可以通过 PageStorage.of 获得到数据桶。


比如 ScrollPosition 就是用该特性,存储滚动的偏移量。

void saveScrollOffset() {  PageStorage.of(context.storageContext)?.writeState(context.storageContext, pixels);}
复制代码


第四层:FocusScope。为页面添加了一个焦点域。


第五层:RepaintBoundary。为页面添加绘制的边界,也就是说这个节点以上的节点是不需要绘制的。


之前我们讲到布局的时候,讲到布局是需要边界的,如果父节点 依赖了 子节点的尺寸信息,那么子节点需要布局的时候,父节点也会被布局。


同样的道理,绘制也是需要边界的。如果一个节点需要被绘制,那么它会看 isRepaintBoundary 值,这个值是 true 的话,就会仅仅绘制自己,就不想上去看 父节点的绘制情况。RepaintBoundary 组件的 isRepaintBoundary 值就是 true


前面的几层都是一个页面的准备工作,下面的第六层就是 真正复杂的地方。


第六层:AnimatedBuilder。这个动画组件,就是给我们 Route 增加了动画效果。如果不熟悉 AnimatedBuilder 可以看前面的动画的文章,AnimatedBuilder 就是给构造方法的 child 参数,增加了一个参数的 builder 效果。动画的驱动器是参数**_listenable**。


我们先拆看。

AnimatedBuilder(  animation: _listenable,   builder: (BuildContext context, Widget child) {    return B;  },  child: A,)
复制代码


我们可以认为,当_listenable 的值(也就是动画的值)发生改变时,页面会显示 B。并且 builder 的 child 参数就是 A。


现在就可以看 A 是谁了。

RepaintBoundary(  key: widget.route._subtreeKey, // immutable  child: Builder(    builder: (BuildContext context) {      return widget.route.buildPage(        context,        widget.route.animation,        widget.route.secondaryAnimation,      );    },  ),)
复制代码


A 是 Builder 组件,所以 A 就是 Route 的 buildPage 方法构造的 Widget。


同理,B 中参数的 child 就是 buildPage。在看 B。

widget.route.buildTransitions(  context,  widget.route.animation,  widget.route.secondaryAnimation,  AnimatedBuilder(    animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),    builder: (BuildContext context, Widget child) {      final bool ignoreEvents = widget.route.animation?.status == AnimationStatus.reverse ||          (widget.route.navigator?.userGestureInProgress ?? false);      focusScopeNode.canRequestFocus = !ignoreEvents;      return IgnorePointer(        ignoring: ignoreEvents,        child: child,      );    },    child: child,  ),)
复制代码


B 就是 Route 的 buildTransitions 构建的 Widget。


B 中又出现了 AnimatedBuilder。这个动画不会印象页面的渲染,只是根据 route 的动画状态,来决定屏不屏蔽手势,用于显示的还是 build。


到这里我们知道了,第六层用到了动画机制,用于动画的组件是 Route 的 buildPage 方法,具体的动画是什么 Route 的 buildTransitions 方法。比如当动画是 0 的时候,我们让 buildTransitions 返回 child,页面显示的就是 Route 的 buildPage。动画是 0.5 的时候,我们让 buildTransitions 返回 Text 文本组件,页面显示的就是 Text 文本组件。动画是 1 的时候,我们让 buildTransitions 返回 child,页面显示的仍然就是 Route 的 buildPage 了。所以根据这个特性,我们就可以显示出平移、缩放等效果了。


至此,ModalRoute.install 就初始化完了,构造了两个 OverlayEntry。一个是遮罩,处理了颜色、点击返回等。一个是_ModalScope,增加了六层节点:实现了基本的传递 Modal 的 Route 信息,焦点,绘制不绘制、数据桶、绘制边界、动画等。


ModalRoute 的初始化,基本就是 Route 的初始化了,功能基本实现。Route 的 buildPage 是用于页面显示的内容,buildTransitions 是内容的添加动画效果的显示。

PageRoute.install

这是页面 Route,它的初始化没有特殊操作,仅仅是对动画控制器做了一点点改动。


@overrideAnimationController createAnimationController() {  final AnimationController controller = super.createAnimationController();  if (settings.isInitialRoute)    controller.value = 1.0;  return controller;}
复制代码


还记得 TransitionRoute.install 吗,它的 install 方法就是创造了动画控制器。


对于一个页面来说,如果他是 isInitialRoute 初始化的 route,其实是不需要动画的,比如 MateriallApp 的 home。

这个时候动画相当于已经完成了,因此动画的值就是 1.0。

_DialogRoute.install

这是对话框的 Route,它的初始化也没有特殊操作。仅仅是明确了动画效果。


@overrideWidget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {  if (_transitionBuilder == null) {    return FadeTransition(        opacity: CurvedAnimation(          parent: animation,          curve: Curves.linear,        ),        child: child);  } // Some default transition  return _transitionBuilder(context, animation, secondaryAnimation, child);}
复制代码


我们知道,install 会间接调用到 buildTransitions 方法,buildTransitions 主要是根据动画的值,构建此动画时刻要显示的内容,这样就可以明确动画的效果是什么了。


我们看到默认的话,就是一个 FadeTransition 动画效果,特效的话是线性的。

小结

我们通过从顶到具体,看了每一层 Route 的初始化过程。我们知道:


  1. Route 中的 OverlayEntry 数组,是要添加 Navigator 的 Overlay 的。所以页面是叠加的。

  2. 每一层有每一层的具化。OverlayRoute 完成了 OverlayEntry 添加,TransitionRoute.install 完成了动画和动画控制器的生成。

  3. ModalRoute.install 是集大成,生成了遮罩和动画效果的联动。buildPage 是显示的内容,buildTransitions 是动画时刻显示的内容。

  4. PageRoute 是页面 Route 的父类,_DialogRoute 是对话框的 Route,提供了默认的 FadeTransition 动画。

Route 层级的线性释放

看了线性的初始化,我们在看线性的释放资源。释放资源就是 dispose 方法。

Route.dispose

@protectedvoid dispose() {  _navigator = null;}
复制代码


不再引用 navigator,避免内存泄漏。

OverlayRoute.dispose

@overridevoid dispose() {  for (OverlayEntry entry in _overlayEntries)    entry.remove();  _overlayEntries.clear();  super.dispose();}
复制代码


与 OverlayRoute 的初始化相对应,OverlayRoute 的初始化中,通过 createOverlayEntries 构造了 OverlayEntry,并且将构造的 entry 插入到了 Overlay 中。


因此,dispose 中就 移除了 OverlayEntry,并清空了 Route 引用的 OverlayEntry。

这里是 Overlay 设计的非常高明的地方,Overlay 不关心 Entry 什么时候移除,Entry 不想背显示了,直接调用他自己的 remove 方法就可以了。这就是:你自己管理你自己

TransitionRoute.dispose

@overridevoid dispose() {  _controller?.dispose();  _transitionCompleter.complete(_result);  super.dispose();}
复制代码


与 TransitionRoute 的初始化相对应,TransitionRoute 构造了动画控制器 Controller,因此 dispose 就将控制器释放掉。

至此,初始化保持的资源已经释放完毕了,后面的也就不需要再次调用了。

总结

至此,我们已经基本明白了 Route 是什么,并且对 Route 的层级体系和结构有了一定的了解。后面我们就可以更加透彻的看 Route 的 push 和 pop 流程了。

发布于: 3 小时前阅读数: 9
用户头像

最强上单

关注

还未添加个人签名 2020.04.03 加入

还未添加个人简介

评论

发布
暂无评论
普普通通的Route