路由与对话框

大多数应用程序具有多个页面或视图,并且希望将用户从页面平滑过渡到另一个页面。Flutter的路由和导航功能可帮助您管理应用中屏幕之间的命名和过渡。

可参考官方文档给出的解释 路由和导航

这里我为什么将路由与对话框放在一起呢?因为这两种本质就是同一个东西,我们学习的时候应当注重知识的内在关联。

管理多个页面时有两个核心概念和类:RouteNavigator。 一个route是一个屏幕或页面的抽象,Navigator是管理route的Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转

我们可以自己创建导航器,但由于我们通常会使用MaterialApp控件,它的内部已经创建了导航器,因此可以使用Navigator.of来获取其内部的导航器对象。

MaterialApphome指定的页面就是导航器堆栈底部的路由。要在堆栈上推送(push)一 个新路由,可以创建一个具有构建器功能的MaterialPageRoute对象,并将想要显示的页面在屏幕上展示出来。

MaterialPageRoute是一种模态路由,在不同平台上可以产生不同过渡动画来切换屏幕。对于Android,页面向上滑动过渡面,并将淡入淡出,弹出过渡则向下滑动页面。在iOS上,页面从右侧滑入,反向弹出。

    /// 压入一个新路由
    Navigator.of(context).push(MaterialPageRoute(
        builder: (BuildContext context) {
          /// 构建一个新的页面
          return Container(
            child: GestureDetector(
              child: Text("确定"),
              /// 弹出这个路由
              onTap: () =>Navigator.of(context).pop(),
            ),
          );
        }
    ));

动态路由

Flutter中的路由通常可分为动态路由和静态路由,像上面示例中使用了路由构建器生成路由的就属于动态路由,它是动态构建的。

有两种动态路由构建器

  • PageRouterBuilder
  • MaterialPageRoute

通常使用MaterialPageRoute即可,但当我需要自定义更加丰富的页面切换动画时,则需要使用PageRouterBuilder

/// 创建一个平移动画
SlideTransition createTransition(Animation<double> animation, Widget child) {
  return SlideTransition(
    position: Tween<Offset>(
      begin: const Offset(1.0, 0.0),
      end: const Offset(0.0, 0.0),
    ).animate(animation),
    child: child,
  );
}

    /// 通过PageRouteBuilder 以指定动画跳转新页面
    Navigator.of(context).push(
        PageRouteBuilder(pageBuilder: (BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation) {
          // 返回新页面
          return NewPage();
        }, transitionsBuilder: (BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,) {
          return createTransition(animation, child);
        }));

静态路由

静态路由也称为命名路由,使用起来更简单,它类似于Web前端中的URL跳转,但它不够灵活,实际项目中的使用并不多。

静态路由的使用分两部分,首先第一部分是注册,类似于配置。

    MaterialApp(
      title: 'Flutter Demo',
      home: HomePage(title: 'Flutter实例'),
      routes: <String, WidgetBuilder> {
        //  配置路由,用一个类似路径的字符串作键
        '/router/second': (_) =>  SecondPage(),
        '/router/profile': (_) =>  ProfilePage(),
      },
    );

第二部分就是在合适的地方进行跳转

// 跳转到已经注册的页面
Navigator.of(context).pushNamed('/router/second');

路由相关的有三个属性需要注意

  • initialRoute: 指定初始路由的名字,即APP启动后应显示的页面
  • onGenerateRoute: 用于生成路由,实际上就是一个匹配器
  • onUnknownRoute: 无法匹配时的回调,类似于Web前端处理的 404 页面

使用中需要注意,initialRoute属性和home属性不应同时设置,它们表达的意义是一样的,只是表现形式不同。如果同时设置了,那么initialRoute的优先级也会高于home

动态路由与静态路由的区别

以上所谓的动态路由和静态路由都只是Flutter中对路由的一种简单封装,以方便开发人员。它们之间最大的区别是,静态路由不能通过页面的构造方法传参,而动态路由可以通过构造方法很方便的将参数传给新页面。另外,如果APP中有太多页面时,使用注册路由表这种方式会显得臃肿而不灵活。

注意,这里有一个常识错误,网络上大量资料说静态路由的缺点是不能传参,实际上是错误的!静态路由只是不能通过构造方法传参而已。

路由传参与返回值

动态路由传参与返回

动态路由中传参给新页面,只需要在构建页面对象的构造方法中传参即可

 int _id = 10;

 /// 等待 push的返回值
 bool result = await Navigator.of(context).push(MaterialPageRoute(
    builder: (BuildContext context) {
      /// 构建一个新的页面
      return NewPage(id:_id);
    }
));

print(result);

/// 在新的页面中,只需要退出时在pop中设置返回值即可
Navigator.of(context).pop(false);

通用的传参方式

以上是一种简单方式,假如需要传递的参数的个数不确定,类型不确定,那么使用构造方法传参就有点麻烦,可能需要创建一个列表传过去。这时候就可以使用更高级的传参方式,这种传参方式的好处是可以支持静态路由传参

Navigator.of(context).push(MaterialPageRoute(
        builder: (context) {
          return NewPage();
        },
        settings: RouteSettings(
          /// 通过一个map来传参
          arguments: {'name': 'postbird'},
        ), // 传参
      ),
);

在新页面中,只需要如下方式获取参数

class NewPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 获取参数
    Map args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
        body: Container()
    );
  }
}

RouteSettings 对象实际上就是路由的基本信息。

匹配器生成路由

MaterialApp(
      title: "Demo",
      initialRoute: '/',
      // 生成匹配的路由
      onGenerateRoute: (RouteSettings settings) {
        switch(settings.name){
          case '/':
            return MaterialPageRoute(builder: (context) => HomePage());
          case '/secondPage':
            return MaterialPageRoute(
                /// 这里直接将参数传入页面的构造方法中
                builder: (context)=> SecondPage(arguments: settings.arguments));
          case '/thirdPage':
            return MaterialPageRoute(
                builder: (context)=> ThirdPage(arguments: settings.arguments));
          default:
            return MaterialPageRoute(builder: (context) => HomePage());
        }
      },
    );

在调用的地方,使用命名路由传参。

Navigator.pushNamed(context, "/secondPage",
                arguments: {'title': "这是一个标题",'id':0});

接下来我们可以在新页面的构造方法中接收这个map,当然,也可以使用ModalRoute来获取。

需要注意,Navigator执行顺序是 initialRoute => onGenerateRoute => onUnknownRoute

路由栈的操作

Navigator 有以下方法

  • push 将新路由入栈,实现页面跳转

  • pushAndRemoveUntil 推送给定路由,删除先前的路由,直到该函数的参数predicate返回true为止

           Navigator.pushAndRemoveUntil(
             context,
             MaterialPageRoute(builder: (BuildContext context) => MyHomePage()),
             ModalRoute.withName('/'),
           );
    
  • pushNamed 将命名路由入栈

  • pushNamedAndRemoveUntilpushAndRemoveUntil ,此处为推送命名路由

  • replace 将指定路由替换成一个新路由

  • pushReplacement 替换指定的路由

  • pushReplacementNamed 替换指定的命名路由

  • pop 当前路由退栈,实现返回到上个页面

  • canPop 判断当前是否可以退栈,如果当前APP在根页面,是不能退栈的

  • maybePop 安全的退栈。相当于canPoppop的组合。

  • popAndPushNamed 指定一个路由路径,并导航到新页面。

  • popUntil 反复执行pop 直到指定的页面为止,即该函数的参数predicate返回true为止

  • removeRoute 立即删除路由,同时执行Route.dispose操作。该方法不接受返回值参数,调用是不运行动画的

  • removeRouteBelow 删除某指定路由下的路由,同时执行Route.dispose

    
        NavigatorState navigator = Navigator.of(context);
        Navigator.of(context).pop();
        Route route = ModalRoute.of(context);
        /// 删除当前路由下的所有路由
        while (navigator.canPop()) navigator.removeRouteBelow(route);
        /// 推入新路由
        await navigator.push(
          MaterialPageRoute(
            builder: (BuildContext context) => MyHomePageThree(),
          ),
        );
    
  • replaceRouteBelow 将指定路由下的路由替换成一个新路由。需注意,被替换的旧的路由必须是当前不可见的,如果要替换最上面可见的路由,应考虑用pushReplacement

需要注意,pushReplacementNamedpopAndPushNamed在效果上是存在区别的,前者只会显示新路由的进入动画,而后者会显示前一个的退栈动画和新路由的入栈动画。

路由监听

当我们需要监听路由的状态时,可以使用路由监听器。路由监听器可分为全局监听和单页监听

全局监听

自定义类继承自NavigatorObserver,实现需要的回调方法

class CustomObserver extends NavigatorObserver{
  @override
  void didPush(Route route, Route previousRoute) {
    super.didPush(route, previousRoute);
  }

  @override
  void didPop(Route route, Route previousRoute) {
    super.didPop(route, previousRoute);
  }
}

我们可以在MaterialAppnavigatorObservers属性中设置多个全局监听器

MaterialApp(
   navigatorObservers: [CustomObserver()],
)

单页监听

还可以在需要监听的页面中动态注册监听器

  CustomObserver customObserver = CustomObserver();

  @override
  void initState() {
    super.initState();
    //在initState时动态添加,避免重复添加
    Navigator.of(context).widget.observers.add(customObserver);
  }

  @override
  void dispose() {
    super.dispose();
    //在dispose时移除监听
    Navigator.of(context).widget.observers.remove(customObserver);
  }

监听器回调

NavigatorObserver类包含以下回调方法

  • didPush push 新路由时回调
  • didPop 弹出路由时回调
  • didRemove 删除路由时回调
  • didReplace 新路由替换旧路由时回调
  • didStartUserGesture 路由被用户以手势移动时回调,例如iOS中的手势返回
  • didStopUserGesturedidStartUserGesture成对出现,当手势结束时回调

简化监听器

对于简单的路由监听功能,直接继承NavigatorObserver类时仍显得繁琐,因为它包含较多回调方法,这时候我们可以继承用法更简洁的RouteObserver类,实际上该类也是继承自NavigatorObserver,仅仅只是封装了部分逻辑。

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> with RouteAware{
  final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();

  @override
  void didPush() {
    super.didPush();
  }

  @override
  void didPop() {
    super.didPop();
  }

  @override
  void didChangeDependencies() {
     super.didChangeDependencies();
     // 注册监听
     routeObserver.subscribe(this, ModalRoute.of(context));
  }

  @override
  void dispose() {
    // 移除监听
    routeObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

只需要在页面的State类上混入RouteAware,并实现需要的回调方法,然后在适当的时候注册监听器即可。官方示例

对话框

对话框是UI界面的一个重要的交互元素,在Flutter中,标准的对话框控件通常就是一个路由

基本对话框

Material 库提供了三种基本对话框控件

  • AlertDialog 通常用于提示型对话框
  • SimpleDialog 通常用于列表型对话框
  • Dialog 通常用于自定义布局元素的对话框

弹出对话框时,调用showDialog函数,将对话框控件传入,由于对话框本身是路由,所以关闭对话框时,需使用Navigator.of(context).pop()

除此外,还有一种iOS风格的基本对话框控件 CupertinoAlertDialog,用法与之类似,使用showCupertinoDialog函数显示,需要注意导入cupertino的包

AlertDialog 示例

                InkWell(
                  onTap: (){
                    showDialog(context: context,child: AlertDialog(
                      title: Text("提示"),
                      content: Text("这是一个对话框控件"),
                      actions: <Widget>[
                        FlatButton(
                          child: Text("取消"),
                          //关闭对话框
                          onPressed: () => Navigator.of(context).pop(),
                        ),
                        FlatButton(
                          child: Text("知悉"),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    ));
                  },)

SimpleDialog 示例

InkWell(
  onTap: () async{
    int i = await showDialog<int>(
        context: context,
        builder: (BuildContext context) {
          return SimpleDialog(
            title: const Text('请选择语言'),
            children: <Widget>[
              SimpleDialogOption(
                onPressed: () {
                  Navigator.pop(context, 1);
                },
                child: Padding(
                  padding: const EdgeInsets.symmetric(vertical: 6),
                  child: const Text('中文简体'),
                ),
              ),
              SimpleDialogOption(
                onPressed: () {
                  Navigator.pop(context, 2);
                },
                child: Padding(
                  padding: const EdgeInsets.symmetric(vertical: 6),
                  child: const Text('中文繁体'),
                ),
              ),
            ],
          );
        });
    print("选择了$i");
  },)

需要注意,AlertDialogSimpleDialog中不能直接使用如ListViewGridViewCustomScrollView等控件,必须给它们包裹一层设置固定的高度,否则会报错。这时候,我们就可以使用Dialog来解决问题

                    showDialog(
                        context: context,
                        builder: (BuildContext context) {
                          return Dialog(
                            child: ListView(
                              children: <Widget>[
                                  Text("item 1"),
                                  Text("item 2"),
                              ],
                            ),
                          );
                        });

模态对话框

使用showModalBottomSheet函数可以弹出模态对话框,与基本对话框一样,它本质上也是一个路由。

                    showModalBottomSheet(
                      context: context,
                      builder: (BuildContext context) {
                        return ListView.builder(
                          itemCount: 20,
                          itemBuilder: (BuildContext context, int index) {
                            return ListTile(
                              title: Text("$index"),
                              onTap: () => Navigator.of(context).pop(),
                            );
                          },
                        );
                      },
                    );

当然,Flutter也提供了一个iOS风格的模态对话框

showCupertinoModalPopup(
    context: context,
    builder: (BuildContext context) {
      return CupertinoActionSheet(
        title: Text('提示'),
        message: Text('这是一个iOS风格的模态对话框'),
        actions: <Widget>[
          CupertinoActionSheetAction(
            child: Text('确定'),
            onPressed: () {},
            isDefaultAction: true,
          ),
          CupertinoActionSheetAction(
            child: Text('暂时取消'),
            onPressed: () {},
            isDestructiveAction: true,
          ),
        ],
      );
    }
);

底部弹框

使用showBottomSheet 函数可以弹出一个底部弹框,这里需要特别声明,实际上showBottomSheet 弹出的并不是一个对话框,只是一个底部弹框,虽然它的名字和showModalBottomSheet相似,但两者有本质区别,这里极容易混淆。

调用该函数会从设备底部向上弹出一个菜单列表

PersistentBottomSheetController _controller;

_controller = showBottomSheet(
    context: context,
    builder: (BuildContext context) {
        return SizedBox(
          height: 250,
          child: ListView.builder(
            itemCount: 10,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(
                title: Text("$index"),
                onTap: (){
                  // 关闭弹框
                  _controller.close();
                },
              );
            },
          ),
        );
    },
  );

两者的区别

  1. showBottomSheet实际上是Scaffold中的方法,必须在Scaffold下使用,且必须获得Scaffoldcontext
  2. showBottomSheet 和 页面框架的悬浮按钮fab 存在组合动画
  3. showModalBottomSheet 是一个对话框路由,因此会占满全屏,未占用的空间会显示半透明蒙层

其他对话框

  • showAboutDialog 关于对话框
  • showDatePicker 日期选择器
  • showTimePicker 时间选择器
  • showCupertinoModalPopup + CupertinoTimerPicker iOS 风格时间选择器
  • showCupertinoModalPopup + CupertinoDatePicker iOS 风格日期选择器

示例

                    var date = DateTime.now();
                    Future<DateTime> result = showCupertinoModalPopup(
                      context: context,
                      builder: (ctx) {
                        return SizedBox(
                          height: 300,
                          child: CupertinoDatePicker(
                            mode: CupertinoDatePickerMode.dateAndTime,
                            minimumDate: date,
                            maximumDate: date.add(
                              Duration(days: 30),
                            ),
                            maximumYear: date.year + 1,
                            onDateTimeChanged: (DateTime value) {
                              print(value);
                            },
                          ),
                        );
                      },
                    );

公众号“编程之路从0到1”

20190301102949549

Copyright © Arcticfox 2020 all right reserved,powered by Gitbook文档修订于: 2022-05-01 12:00:55

results matching ""

    No results matching ""