Flutter之旅:路由導航

這章來聊聊flutter的路由管理,也可以理解為頁面導航,用來處理頁面之間的跳轉、參數(shù)傳遞、動畫展示等功能。

路由導航主要由跳轉和返回兩個操作,跳轉是調用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的默認實現(xiàn)均為私有,就是說如果以后要用到的話需要我們自己去實現(xiàn)。PageRoute默認提供了三個公開的實現(xiàn)類:

  • CupertinoPageRoute:Cupertino風格的默認實現(xiàn)。
  • MaterialPageRoute:Material風格的默認實現(xiàn)。
  • PageRouteBuilder:自定義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

替換當前頁面,并且當新頁面動畫執(zhí)行完成之后,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);
  }

前兩個參數(shù)同push,第三個可選參數(shù)result表示的是這個頁面的返回結果,如果設置的話,會返回給被替換的這個頁面的前一個頁面。

我們可以做這樣一個操作:

  • 在Page1調用push方法跳轉到Page2,并監(jiān)聽結果
  • 在Page2調用pushReplacement方法跳轉Page3,并設置result


    route_2.gif

得到的日志如下:

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

跳轉到指定頁面,并按順序(從棧頂?shù)綏5祝┮瞥鲋暗乃许撁?,直到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已經(jīng)返回true。


route_4.gif

pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil

三者分別對應push、pushReplacement、pushAndRemoveUntil,提供了一種命名路由跳轉,并且在flutter新版本中增加了一個可選參數(shù)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參數(shù),可以直接使用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);
  }

可以接兩個參數(shù):

  • context:上下文。
  • result:即我們前面提到的返回給上一個頁面的值。

popUntil

按順序從棧內移除最頂上的頁面,直到predicate返回true。predicate參數(shù)的含義可以參照上面的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);
  }

頁面?zhèn)鲄?/h1>

如果是非命名路由,即push系列方法,直接使用路由的構造函數(shù)傳參即可:

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

如果是命名路由,之前是不可以傳參的,新版本中增加了一個arguments參數(shù),配合onGenerateRoute也可以傳遞參數(shù),因為命名路由會將路由的名字傳遞給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

路由相關設置,名字、參數(shù)、是否初始路由,如果為空,則會生成一個默認的。

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

pageBuilder

用來構建路由的主要內容??梢圆榭碝odalRoute.buildPage方法來了解它的參數(shù)信息。

typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
  • context:正在構建的路由的上下文。
  • animation:路由的主變換動畫,如果是進入,值從0.0逐漸變化到1.0;如果是退出,值從1.0逐漸變化到0.0。
  • secondaryAnimation:路由的次變換動畫。

transitionsBuilder

用于構建路由的變換效果??梢酝ㄟ^ModalRoute.buildTransitions方法來了解它的參數(shù)信息。

typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
  • context:正在構建的路由的上下文。
  • animation:路由的主變換動畫,如果是進入,值從0.0逐漸變化到1.0;如果是退出,值從1.0逐漸變化到0.0。
  • secondaryAnimation:路由的次變換動畫。當把一個新的路由push到棧頂時,原棧頂?shù)穆酚傻膕econdaryAnimation值從0.0變化到1.0;當棧頂路由被pop的時候,它下面的那個路由的secondaryAnimation值從1.0變化到0.0。
  • child:頁面的內容,即pageBuilder返回的widget。

transitionDuration

變換效果的持續(xù)時間。

opaque

是否不透明,默認為true,如果是不透明的話,路由變換完成之后,不會再構建位于該路由之下的路由,以節(jié)省資源。

barrierColor

模態(tài)屏障的顏色。如果為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狀態(tài)時,是否需要在內存中保存路由狀態(tài)。

示例

我們來做一個簡單的旋轉漸隱的動畫效果。

                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中的簡單實現(xiàn)。

使用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

完整代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容