Flutter:使用 Navigator 推送到新屏幕时保持 BottomNavigationBar

Flutter: Keep BottomNavigationBar When Push to New Screen with Navigator

在 iOS 中,我们有一个 UITabBarController,当我们推送到新的 ViewController 时,它会永久停留在屏幕底部。

在Flutter中,我们有一个Scaffold的bottomNavigationBar。但是,与 iOS 不同的是,当我们 Navigator.push 到一个新屏幕时,这个 bottomNavigationBar 消失了。

在我的应用中,我想满足这个要求:主屏幕有一个 bottomNavigationBar 和 2 个项目(a & b) 显示屏幕 A & B。默认情况下,显示屏幕 A。在屏幕A里面,有一个按钮。点击那个按钮,Navigator.push 到屏幕 C。现在在屏幕 C 中,我们仍然可以看到 bottomNavigationBar。点击项目 b,我转到屏幕 B。现在在屏幕 B 中,点击 bottomNavigationBar 中的项目 a,我返回屏幕 C(不是 AA 当前在导航层次结构中低于 C)。

我该怎么做?谢谢大家。

编辑:我添加了一些图片用于演示:

画面A Screen A

点击转到C按钮,推到屏幕C Screen C

点击 底部导航栏中的项目,转到屏幕 B Screen B

我认为#right 的做法是在两种情况下使用相同的标签将 BottomNavigationBar 包裹在 Hero 中。这样,当页面之间的动画发生时,它们将被排除。

这是我能做的尽可能简短的示例,但我强烈建议您对其进行清理,即传递英雄字符串,使用小部件而不是大量构建,为 BottomNavigationBar 制作您自己的小部件。

请注意,在英雄转换期间,它至少在我的 phone 上 overflow by 0.0000191 pixels,但在发布模式下,我认为这不是问题。

导入'package:flutter/material.dart';

void main() => runApp(new MaterialApp(
      home: new Builder(
        builder: (context) => new Scaffold(
              bottomNavigationBar: new Hero(
                tag: "bottomNavigationBar",
                child: new BottomNavigationBar(items: [
                  new BottomNavigationBarItem(icon: new Icon(Icons.home), title: new Text("Home")),
                  new BottomNavigationBarItem(icon: new Icon(Icons.ac_unit), title: new Text("AC Unit"))
                ]),
              ),
              body: new SafeArea(
                child: new Container(
                  constraints: new BoxConstraints.expand(),
                  color: Colors.green,
                  child: new Column(
                    children: <Widget>[
                      new RaisedButton(
                          child: new Text("Press me"),
                          onPressed: () {
                            Navigator.push(
                                context,
                                new MaterialPageRoute(
                                    builder: (context) => new Scaffold(
                                          bottomNavigationBar: new Hero(
                                            tag: "bottomNavigationBar",
                                            child: new BottomNavigationBar(items: [
                                              new BottomNavigationBarItem(icon: new Icon(Icons.home), title: new Text("Home")),
                                              new BottomNavigationBarItem(icon: new Icon(Icons.ac_unit), title: new Text("AC Unit"))
                                            ]),
                                          ),
                                          body: new SafeArea(
                                            child: new Container(
                                              constraints:
                                                  new BoxConstraints.expand(),
                                              color: Colors.red,
                                              child: new Column(
                                                children: <Widget>[
                                                  new RaisedButton(
                                                    onPressed: () =>
                                                        Navigator.pop(context),
                                                    child: new Text("Back"),
                                                  )
                                                ],
                                              ),
                                            ),
                                          ),
                                        )));
                          })
                    ],
                  ),
                ),
              ),
            ),
      ),
    ));

我不知道 hero 系统处理多个英雄等的效果如何,如果你说想要为导航栏设置动画,这可能不太好。

还有另一种方法可以使底部导航栏具有动画效果;这实际上是一个已经回答的问题:

tl;dr:使用 CupertinoTabBar with CupertinoTabScaffold

问题不在 Flutter 中,而是在 UX 中,就像 Rémi Rousselet 提到的那样。

原来Material设计不建议sub-pages在层次结构中访问底部导航栏。

但是,iOS 人机界面指南建议这样做。因此,要使用此功能,我必须调整 Cupertino 小部件而不是 Material 小部件。具体来说,在 main 中,return 一个 WidgetsApp/MaterialApp 其中包含一个 CupertinoTabScaffold。使用 CupertinoTabBar 实现标签栏,每个屏幕都是 CupertinoTabView.

另一种实现此目的的方法(虽然不是好的做法)是在脚手架的主体中嵌套一个 material 应用程序。并在那里处理所有 "sub-navigation"。

因此,您的层次结构将如下所示

Material App
  - home
     - Scaffold
       - body
         - Material App
              - Scaffold
                  - AppBar
                  - body
                  ...
         - routes (internal)
       - bottomNavigationBar
  - routes (external)

我试过了,效果很好。不幸的是我现在无法 post 源代码。

你实际上可以在正文中放置一个占位符 所以这样的结构

- AppBar
- body (dynamic content from placeholder)
- BottomNavigationBar

然后你会有另一个 class 作为占位符 所以每次你点击 BottomNavigationBar 它都会刷新正文的内容

这里是我找到的一个例子 https://willowtreeapps.com/ideas/how-to-use-flutter-to-build-an-app-with-bottom-navigation

这里有点太复杂了,不适合我 https://medium.com/@swav.kulinski/flutter-navigating-off-the-charts-e118562a36a5

还有这个 https://medium.com/coding-with-flutter/flutter-case-study-multiple-navigators-with-bottomnavigationbar-90eb6caa6dbf

您可以在 Stack 小部件中创建 Navigator 小部件,以将 BottomNavigationBar 用于选项卡的内部导航。您可以使用 WillPopScope 来处理 Android 的后退按钮以弹出选项卡的内部屏幕。此外,双击底部导航项可弹出标签页的所有内部屏幕。

我创建了一个 Sample app 为此。

希望对您有所帮助!

选项 1:如果您只想保留 BottomNavigationBar,则尝试使用 this

选项 2:使用 CupertinoTabBar,如下所示用于静态 BottomNavigationBar

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mqttdemo/Screen2.dart';
import 'package:mqttdemo/Screen3.dart';

import 'Screen1.dart';

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int _currentIndex;
  List<Widget> _children;

  @override
  void initState() {
    _currentIndex = 0;
    _children = [
      Screen1(),
      Screen2(),
      Screen3(),
    ];
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        currentIndex: _currentIndex,
        onTap: onTabTapped,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text("Screen 1"),
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            title: Text("Screen 2"),
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.home), title: Text("Screen 3")),
        ],

      ),
        tabBuilder: (BuildContext context, int index) {
          return CupertinoTabView(
            builder: (BuildContext context) {
              return SafeArea(
                top: false,
                bottom: false,
                child: CupertinoApp(
                  home: CupertinoPageScaffold(
                    resizeToAvoidBottomInset: false,
                    child: _children[_currentIndex],
                  ),
                ),
              );
            },
          );
        }
    );
  }

  void onTabTapped(int index) {
    setState(() {
      _currentIndex = index;
    });
  }
}

从 Screen3 导航到 screen4,如下所示:

    class Screen3 extends StatefulWidget {
      @override
      _Screen3State createState() => _Screen3State();
    }
    
    class _Screen3State extends State<Screen3> {
      @override
      Widget build(BuildContext context) {
        return Container(
          color: Colors.black,
          child: Center(
            child: RaisedButton(
              child: Text("Click me"),
              onPressed: () {
                Navigator.of(context, rootNavigator: false).push(MaterialPageRoute(
                    builder: (context) => Screen4(), maintainState: false));
              },
            ),
          ),
        );
      }

}

截图:


起点:

void main() => runApp(MaterialApp(home: HomePage()));

HomePage [BottomNavigationBar + Page1]

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        backgroundColor: Colors.orange,
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.call), label: 'Call'),
          BottomNavigationBarItem(icon: Icon(Icons.message), label: 'Message'),
        ],
      ),
      body: Navigator(
        onGenerateRoute: (settings) {
          Widget page = Page1();
          if (settings.name == 'page2') page = Page2();
          return MaterialPageRoute(builder: (_) => page);
        },
      ),
    );
  }
}

第 1 页:

class Page1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page1')),
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.pushNamed(context, 'page2'),
          child: Text('Go to Page2'),
        ),
      ),
    );
  }
}

第2页:

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}