Flutter圈子Flutter中文社区

Flutter之旅:路由导航

2019-03-08  本文已影响57人  风少侠

这章来聊聊flutter的路由管理,也可以理解为页面导航,用来处理页面之间的跳转、参数传递、动画展示等功能。

路由导航主要由跳转和返回两个操作,跳转是调用Navigator的push相关方法,返回是调用Navigator的pop相关方法,可以理解为push是将一个页面推送到路由栈中,pop是将一个页面从栈中移出。

push相关

先看一下Navigator中push的相关方法:


push.png

push

Navigator.push(context,Route);

  @optionalTypeArgs
  static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
  }

该方法接受一个BuildContext和一个Route,context就不用说了,下面了解一下Route:


Route.png

简单一点看,Route分为了页面路由(PageRoute)和窗口路由(PopupRoute),而PopupRoute的默认实现均为私有,就是说如果以后要用到的话需要我们自己去实现。PageRoute默认提供了三个公开的实现类:

示例代码:

Navigator.push(context,MaterialPageRoute(builder: (context) => Page2()));

另外push相关方法返回的都是一个Future,可以通过它来获取下一个页面的被pop时的返回值。

Navigator.push(context,MaterialPageRoute(builder: (context) => Page2()))
     .then((value) {
        print('page1 push $value');
});

完整代码

route_1.gif

pushReplacement

替换当前页面,并且当新页面动画执行完成之后,disposing前一个页面。

Navigator.pushReplacement(context,MaterialPageRoute(builder: (context) => Page3()));

源码:

  @optionalTypeArgs
  static Future<T> pushReplacement<T extends Object, TO extends Object>(BuildContext context, Route<T> newRoute, { TO result }) {
    return Navigator.of(context).pushReplacement<T, TO>(newRoute, result: result);
  }

前两个参数同push,第三个可选参数result表示的是这个页面的返回结果,如果设置的话,会返回给被替换的这个页面的前一个页面

我们可以做这样一个操作:

得到的日志如下:

I/flutter (14537): Page1 build
I/flutter (14537): Page1 push Page2
I/flutter (14537): Page2 build
I/flutter (14537): Page2 pushReplacement Page3 and result: Page2 result
I/flutter (14537): Page3 build
I/flutter (14537): Page1 push result: Page2 result

完整代码

pushAndRemoveUntil

跳转到指定页面,并按顺序(从栈顶到栈底)移出之前的所有页面,直到predicate返回true。

  @optionalTypeArgs
  static Future<T> pushAndRemoveUntil<T extends Object>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
    return Navigator.of(context).pushAndRemoveUntil<T>(newRoute, predicate);
  }

typedef RoutePredicate = bool Function(Route<dynamic> route);

比如我从Page2调用跳转pushAndRemoveUntil到Page3,同时指定predicate的条件为route.settings.name == "/",那么跳转到Page3后Page2将被移除,因为第一个页面的默认RouteSetting的name属性值为"/"。

                Navigator.pushAndRemoveUntil(
                        context,
                        MaterialPageRoute(builder: (context) => Page3()),
                        (route) {
                          print('route:$route');
                          return route.settings.name == "/";
                        })
                    .then((value) {
                  print('Page2 pushAndRemoveUntil result: $value');
                });
route_3.gif

如果predicate的条件为route.settings.name != "/",那么任何一个页面都不会被移除,因为判断第一个前页面Page2的时候predicate已经返回true。


route_4.gif

pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil

三者分别对应push、pushReplacement、pushAndRemoveUntil,提供了一种命名路由跳转,并且在flutter新版本中增加了一个可选参数arguments,用于页面之间的传参。路由的名字将会传递给Navigator的onGenerateRoute回调,并将返回的路由推入Navigator栈(具体可见下面的传参部分)。

这里以pushNamed方法为例,首先声明一个路由列表:

const String PAGE_2 = "/page2";

final Map<String, WidgetBuilder> _routes = {
  PAGE_2: (_) => Page2(),
};

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(routes: _routes, home: Page1());
  }
}

跳转的时候直接调用pushNamed传入路由对应的键值即可:

Navigator.pushNamed(context, PAGE_2);

对于pushNamedAndRemoveUntil的predicate参数,可以直接使用ModalRoute.withName(name)来指定。

pop相关

还是先看一下pop的相关方法:


pop.png

pop

从栈内移除最顶上的页面。

  @optionalTypeArgs
  static bool pop<T extends Object>(BuildContext context, [ T result ]) {
    return Navigator.of(context).pop<T>(result);
  }

可以接两个参数:

popUntil

按顺序从栈内移除最顶上的页面,直到predicate返回true。predicate参数的含义可以参照上面的pushAndRemoveUntil。

  static void popUntil(BuildContext context, RoutePredicate predicate) {
    Navigator.of(context).popUntil(predicate);
  }

popAndPushNamed

就是pop和pushNamed两个方法的组合。

  @optionalTypeArgs
  Future<T> popAndPushNamed<T extends Object, TO extends Object>(
    String routeName, {
    TO result,
    Object arguments,
  }) {
    pop<TO>(result);
    return pushNamed<T>(routeName, arguments: arguments);
  }

页面传参

如果是非命名路由,即push系列方法,直接使用路由的构造函数传参即可:

Navigator.push(context, MaterialPageRoute(builder: (context) => Page2(arguments: arguments)));

如果是命名路由,之前是不可以传参的,新版本中增加了一个arguments参数,配合onGenerateRoute也可以传递参数,因为命名路由会将路由的名字传递给onGenerateRoute回调,并将产生的路由推入Navigator。

const String PAGE_2 = "/page2";
const String PAGE_3 = "/page3";

final Map<String, Function> _routes = {
  PAGE_2: (context, {arguments}) => Page2(arguments: arguments),
  PAGE_3: (context, {arguments}) => Page3(arguments: arguments),
};

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Page1(),
      onGenerateRoute: (routeSetting) {
        Function _routeGenerate = _routes[routeSetting.name];
        if (_routeGenerate != null)
          return MaterialPageRoute(
              builder: (context) => _routeGenerate(context, arguments: routeSetting.arguments));
      },
    );
  }
}
class Page2 extends StatelessWidget {
  Map<String, Object> arguments;
  Page2({this.arguments});
    @override
  Widget build(BuildContext context) {
    print('Page2 build');
    print('arguments:$arguments');
    ...
  }
}
Navigator.pushNamed(context, PAGE_2,arguments: {"name":"lili"});

得到日志如下:

I/flutter (24397): Page1 pushNamed Page2
I/flutter (24397): Page2 build
I/flutter (24397): arguments:{name: lili}

切换动画

如果想自定义页面的切换效果,我们可以使用PageRouteBuilder来自定义路由。

  PageRouteBuilder({
    RouteSettings settings,
    @required this.pageBuilder,
    this.transitionsBuilder = _defaultTransitionsBuilder,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.opaque = true,
    this.barrierDismissible = false,
    this.barrierColor,
    this.barrierLabel,
    this.maintainState = true,
  }) : assert(pageBuilder != null),
       assert(transitionsBuilder != null),
       assert(barrierDismissible != null),
       assert(maintainState != null),
       assert(opaque != null),
       super(settings: settings);

settings

路由相关设置,名字、参数、是否初始路由,如果为空,则会生成一个默认的。

Route({ RouteSettings settings }) : settings = settings ?? const RouteSettings();

pageBuilder

用来构建路由的主要内容。可以查看ModalRoute.buildPage方法来了解它的参数信息。

typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);

transitionsBuilder

用于构建路由的变换效果。可以通过ModalRoute.buildTransitions方法来了解它的参数信息。

typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);

transitionDuration

变换效果的持续时间。

opaque

是否不透明,默认为true,如果是不透明的话,路由变换完成之后,不会再构建位于该路由之下的路由,以节省资源。

barrierColor

模态屏障的颜色。如果为null,则屏障将是透明的。比如弹出一个对话框时,背景可以设置成灰暗的。注意Dialog也是一个路由。

Future<T> showGeneralDialog<T>({
  @required BuildContext context,
  @required RoutePageBuilder pageBuilder,
  bool barrierDismissible,
  String barrierLabel,
  Color barrierColor,
  Duration transitionDuration,
  RouteTransitionsBuilder transitionBuilder,
}) {
  assert(pageBuilder != null);
  assert(!barrierDismissible || barrierLabel != null);
  return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
  ));
}

可以看到其实Dialog就是一个_DialogRoute。

barrierDismissible

点击屏障是否自动消失。
我们来弹出一个Dialog验证一下,设置屏障颜色为半透明红色,点击屏障自动消失:

                showGeneralDialog(
                    context: context,
                    barrierDismissible: true,
                    barrierLabel: "Dismiss",
                    barrierColor: Color.fromRGBO(255, 0, 0, 0.5),
                    transitionDuration: Duration(milliseconds: 300),
                    pageBuilder: (context, animation, secondaryAnimation) {
                      return AlertDialog(
                        title: Text("标题"),
                      );
                    });
route_5.gif

maintainState

当路由为inactive状态时,是否需要在内存中保存路由状态。

示例

我们来做一个简单的旋转渐隐的动画效果。

                Navigator.push(
                    context,
                    PageRouteBuilder(
                        pageBuilder: (context, animation, secondaryAnimation) {
                          return Page2();
                        },
                        transitionsBuilder:
                            (context, animation, secondaryAnimation, child) {
                          return FadeTransition(
                            opacity: animation,
                            child: RotationTransition(
                              turns: Tween(begin: 0.0, end: 1.0)
                                  .animate(animation),
                              child: child,
                            ),
                          );
                        },
                        transitionDuration: Duration(milliseconds: 500)));
route_6.gif

共享元素动画

做过android的对这个一定不陌生,这里提一下在flutter中的简单实现。

使用Hero包裹要共享的widget,并设置相同的tag。

            Hero(
                tag: "btnBack",
                child: RaisedButton(
                  onPressed: () {
                    print('Page2 pop');
                    Navigator.pop(context);
                  },
                  child: Text("返回"),
                )),

...

            Hero(
                tag: "btnBack",
                child: RaisedButton(
                  onPressed: () {
                    print('Page3 pop');
                    Navigator.pop(context);
                  },
                  child: Text("返回"),
                )),
route_7.gif

完整代码

上一篇下一篇

猜你喜欢

热点阅读