Flutter:使用导航器推送到新屏幕时保持底部导航栏
Posted
技术标签:
【中文标题】Flutter:使用导航器推送到新屏幕时保持底部导航栏【英文标题】:Flutter: Keep BottomNavigationBar When Push to New Screen with Navigator 【发布时间】:2018-09-12 16:30:09 【问题描述】:在 ios 中,当我们推送到新的 ViewController 时,我们有一个 UITabBarController 永久保持在屏幕底部。
在 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(不是A,A 当前在导航层次结构中位于 C 之下)。
我该怎么做?谢谢各位。
编辑:我包含一些图片用于演示:
屏幕 A Screen A
点击转到C按钮,推到屏幕C Screen C
点击底部导航栏中的右项,转到屏幕B Screen B
【问题讨论】:
你所说的button
是在BottomNavigationBar
里面吗?
不,按钮不在底部栏中。它在主屏幕内。这只是触发Navigator
到push
到新屏幕的东西。
我认为这更像是一个用户体验问题。因为你的 C 视图不应该有底部导航栏。或者,应该可以从该底部栏访问 C。
同一层次中的屏幕能够拥有底部导航栏不是很常见吗?以 Twitter 为例(请打开 iOS Twitter 应用程序),点击一条推文,TweetViewController
被推送,底部栏仍然可见。我认为几乎所有流行的应用都有这种行为。
我同意 Harry 的说法,这在 iOS 中是很常见的事情,而 TBH Flutter 为整个屏幕设置动画的方式实际上与 iOS 处理导航栏的方式有点相反——尽管 iOS modal popup
输入屏幕时会覆盖导航栏。
【参考方案1】:
tl;dr:使用 CupertinoTabBar 和 CupertinoTabScaffold
问题不在于 Flutter,而在于 UX,就像 Rémi Rousselet 提到的那样。
事实证明,Material Design 不推荐层次结构中的子页面访问底部导航栏。
但是,iOS 人机界面指南建议这样做。因此,要使用此功能,我必须调整 Cupertino 小部件而不是 Material 小部件。具体来说,主要返回一个WidgetsApp/MaterialApp
,其中包含一个CupertinoTabScaffold
。使用CupertinoTabBar
实现标签栏,每个屏幕都是CupertinoTabView
。
【讨论】:
Material Design 现在建议在子页面中导航,根据 github.com/flutter/flutter/issues/16221 和 material.io/components/bottom-navigation#behavior 只是还没有实现... 现在实施了吗?我仍然只能找到 hacky 解决方法或使用 3rd 方包【参考方案2】:截图:
起点:
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);
,
),
);
第一页:
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'),
),
),
);
第二页:
class Page2 extends StatelessWidget
@override
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
【讨论】:
像魅力一样工作。非常感谢 找不到路由RouteSettings的生成器 以及如何在没有底部导航栏的情况下导航一些屏幕,你能解释一下吗? 您可以使用root navigator:Navigator.of(context, rootNavigator: true).pushFunction(...)
,但您还需要在root中配置路由,例如在materialApp中设置routes
属性
@KunYuTsai 将onChanged
属性添加到BottomNavigationBar
小部件,使用索引来跟踪当前页面,然后使用if-else
条件,例如,选择要使用哪个小部件显示。【参考方案3】:
您实际上可以在正文中放置一个占位符 所以结构是这样的
- AppBar
- body (dynamic content from placeholder)
- BottomNavigationBar
然后你会有另一个类作为占位符 所以每次点击 BottomNavigationBar 都会刷新 body 的内容
我发现的一个例子是here 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
【讨论】:
【参考方案4】:选项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));
,
),
),
);
【讨论】:
【参考方案5】:实现此目的的另一种方法(虽然不是很好的做法)是将材质应用程序嵌套在脚手架的主体中。并在那里处理所有“子导航”。
因此,您的层次结构将如下所示
Material App
- home
- Scaffold
- body
- Material App
- Scaffold
- AppBar
- body
...
- routes (internal)
- bottomNavigationBar
- routes (external)
我试过了,效果很好。很遗憾,我现在无法发布源代码。
【讨论】:
【参考方案6】:您可以在Stack
小部件中创建Navigator
小部件,以将BottomNavigationBar
与标签的内部导航一起使用。您可以使用WillPopScope
处理android 的后退按钮以弹出选项卡的内部屏幕。此外,双击底部导航项以弹出选项卡的所有内部屏幕。
我创建了一个Sample app 为此。
希望对您有所帮助!
【讨论】:
【参考方案7】:我认为#right 这样做的方法是在两种情况下都将BottomNavigationBar 包裹在Hero 中,并使用相同的标签。这样,当页面之间的动画发生时,它们将被排除在外。
这是一个尽可能简短的示例,但我强烈建议清理它,即传入 hero 字符串,使用小部件而不是大量构建块,为 BottomNavigationBar 制作自己的小部件。
请注意,在英雄转换期间,它至少在我的手机上会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
系统对多个英雄等的处理效果如何,如果你说想要为导航栏设置动画,这可能效果不佳。
还有另一种方法可以让您为底部导航栏设置动画;这实际上是一个已经回答的问题:Flutter: Hero transition + widget animation at the same time?
【讨论】:
以上是关于Flutter:使用导航器推送到新屏幕时保持底部导航栏的主要内容,如果未能解决你的问题,请参考以下文章
WIX Navigation V2 - 推送到新屏幕时隐藏底部标签栏