Flutter 之 Scaffold、TabBar、底部导航、抽屉菜单

Material 库提供了很多的 Widget,本节介绍一些常用的 Widget,如:Scaffold、TabBar、TabBarView、Drawer、BottomNavigationBar等。其余的可以参看Flutter 实战示例

Scaffold


大多数路由页都会包含一个导航栏,有些路由页可能会有抽屉(Drawer)菜单及底部 Tab 导航菜单等。Flutter Material 库提供了一个 Scaffold Widget,它是一个路由页的骨架,可以非常容易的拼装出一个完整的页面。

Scaffold 实现了基本的 Material Design 布局。 只要是在 Material Design 中定义过的单个界面显示的布局组件元素,都可以使用 Scaffold 来绘制 。

Scaffold 实现了一个基本的 material design 的布局结构;

// Scaffold 源码结构
class Scaffold extends StatefulWidget {
  /// Creates a visual scaffold for material design widgets.
  const Scaffold({
    Key key,
    this.appBar,//标题栏,显示在界面顶部的一个 AppBar,也就是 Android 中的 ActionBar 、Toolbar
    this.body,//当前界面所显示的主要内容 Widget
    this.floatingActionButton,//悬浮按钮
    this.floatingActionButtonLocation,//悬浮按钮位置
    this.floatingActionButtonAnimator,//悬浮按钮动画
    this.persistentFooterButtons,//固定在下方显示的按钮,比如对话框下方的确定、取消按钮
    this.drawer,//侧滑菜单左
    this.endDrawer,//侧滑菜单右
    this.bottomNavigationBar,//底部导航
    this.bottomSheet,
    this.backgroundColor,//内容的背景颜色,默认使用的是 ThemeData.scaffoldBackgroundColor 的值
    this.resizeToAvoidBottomPadding,//类似于 Android 中的 android:windowSoftInputMode=”adjustResize”,控制界面内容 body 是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。
    this.resizeToAvoidBottomInset,
    this.primary = true,//试用使用primary主色
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
  }) : assert(primary != null),
       assert(extendBody != null),
       assert(drawerDragStartBehavior != null),
       super(key: key);

Scaffold组件常见属性如表所示。

属性名 类型 说明
appBar AppBar 显示在界面顶部的一个 AppBar
body Widget 当前界面所显示的主要内容
floatingActionButton Widget 在 MaterialDesign 中定义的一个功能按钮
persistentFooterButtons List<Widget> 固定在下方显示的按钮
drawer Widget 侧边栏组件
bottomNavigationBar Widget 显示在底部的导航栏按钮栏
backgroundColor Color 背景颜色
resizeToAvoidBottomPadding bool 控制界面内容 body 是否重新布局来避免底部被覆盖, 比如当键盘显示时,重新布局避免被键盘盖住 内容。 默认值为 true

从构造方法我们可以看到,Scaffold 可以帮助我们事先类似于 Android 中 toolbar、悬浮按钮、抽屉菜单、底部导航效果。

示例最终效果如下:


实现代码如下:

class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _selectedIndex = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 头部元素比如 . 左侧返回按钮中问标题右侧菜单
      appBar: AppBar( //导航栏
        title: Text("App Name"), 
        actions: <Widget>[ //导航栏右侧菜单
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
      ),
      drawer: new MyDrawer(), //抽屉
      bottomNavigationBar: BottomNavigationBar( // 底部导航
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton( //悬浮按钮
          child: Icon(Icons.add),
          onPressed:_onAdd
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){
  }
}

上面代码中我们用到了另外几个Widget,下面我们来分别介绍一下:

AppBar


应用按钮组件有 AppBar 和 SliverAppBar。 它们是墨设计中的 AppBar,也就是 Android 中的 Toolbar。

AppBar和 SliverAppBar 都 是继承自 StatefulWidget 类,都代表 Toolbar,两者的区别在于 AppBar 位置是固定在应用最上面的;而 SliverAppBar 是可以跟随内容滚动的。。

AppBar 是页面最上面用来显示页面状态或者信息的导航条;

AppBar 是一个Material风格的导航栏,它可以设置标题、导航栏菜单、底部Tab等。下面我们看看AppBar的定义:

AppBar({
    Key key,
    this.leading,//导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
    this.automaticallyImplyLeading = true,//如果leading为null,是否自动实现默认的leading按钮
    this.title,// Toolbar 中主要内容,通常显示为当前界面的标题文字
    this.actions,// 导航栏右侧菜单;一个 Widget 列表,代表 Toolbar 中所显示的菜单,对于常用的菜单,通常使用 IconButton 来表示;对于不常用的菜单通常使用 PopupMenuButton 来显示为三个点,点击后弹出二级菜单
    this.flexibleSpace,
    this.bottom,// 导航栏底部菜单,通常是 TabBar。用来在 Toolbar 标题下面显示一个 Tab 导航栏
    this.elevation,// 导航栏阴影
    this.shape,
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.actionsIconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,//标题是否居中 
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,//title与leading的间隔
    this.toolbarOpacity = 1.0,//title级文字透明度
    this.bottomOpacity = 1.0,//底部文字透明度
  }) : assert(automaticallyImplyLeading != null),
       assert(elevation == null || elevation >= 0.0),
       assert(primary != null),
       assert(titleSpacing != null),
       assert(toolbarOpacity != null),
       assert(bottomOpacity != null),
       preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
       super(key: key);

AppBar 及 SliverAppBar 组件常见属性见表:

属性名 类型 默认值 说明
leading Widget null 在标题前面显示的一个组件,在首页通常显示应用的 logo; 在其他界面通常显示为返回按钮
title Widget null Toolbar 中主要 内容 ,通常显示为当前界面的标题文字
actions List<Widget> null 一个 Widget列表,代表 Toolbar中所显示的 菜单,对于常 用的 菜 单,通常使用 IconButton 来表示,对于不常用的菜单通常使用 Popup- MenuButton j来显示为三 个点,点击后弹出 二级 菜单
bottom PreferredSize Widget null 通常是 TabBar。 用来在 Toolbar 标题下面显示 一个 Tab 导航栏
elevation double 4 纸墨设计中组件的 z 坐标顺序,对于可滚动的 SliverAppBar,当 SliverAppBar和内容同级的 时候,该值为 0,当内容滚动 SliverAppBar变为 Toolbar的时候,修改 elevatio口的值
flexibleSpace Widget null 一个显示在 AppBar下 方 的 组 件, 高度和 AppBar 高度一样 , 可以实现一些特殊的效果, 该属性通常’在 SliverAppBar 中使用
brightness Brightness ThemeData. primaryColorBrightness AppBar 的亮度 ,有白色和黑色两种主题
backgroundColor Color ThemeData. primaryColor 背景色
iconTheme IconThemeData ThemeData primaryIconTheme AppBar 上图标的颜色 、 透明度和尺寸信息 。默认值为 ThemeData.primaryIconTheme
textTheme TextTheme Them巳Data. primaryTextTheme AppBar上的文字样式
centerTitle bool true 标题是否居 中显示,默认值根据不 同的操作 系统 , 显示方式不一样

AppBar可以显示顶部 leading、 title 和 actions 等内容。 底部通常为选项卡 TabBar。 flexibleSpace 显示在 AppBar 的下方,高度和 AppBar 高度一样,可以实现一些特殊的效果, 不过该属性通常在 SliverAppBar 中使用。具体布局如图所示。


AppBar组件布局图

如果给 Scaffold 添加了抽屉菜单,默认情况下 Scaffold 会自动将 AppBar 的 leading 设置为菜单按钮(如上面截图所示)。如果我们想自定义菜单图标,可以手动来设置leading,如

Scaffold(
  appBar: AppBar(
    title: Text("App Name"),
    leading: Builder(builder: (context) {
      return IconButton(
        icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标
        onPressed: () {
          // 打开抽屉菜单  
          Scaffold.of(context).openDrawer(); 
        },
      );
    }),
    ...  
  )

代码运行效果:


可以看到左侧菜单已经替换成功。

代码中打开抽屉菜单的方法在 ScaffoldState 中,通过 Scaffold.of(context) 可以获取父级最近的 Scaffold Widget 的 State 对象。Flutter 还有一种通用的获取StatefulWidget 对象 State 的方法:通过 GlobalKey 来获取! 步骤有两步:

  1. 给目标 StatefulWidget 添加 GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= new GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)
  1. 通过GlobalKey来获取State对象
_globalKey.currentState.openDrawer()

TabBar

下面我们看看 TabBar 的定义:

const TabBar({
    Key key,
    @required this.tabs,//显示的标签内容,一般使用Tab对象,也可以是其他的Widget
    this.controller,//TabController对象
    this.isScrollable = false,//是否可滚动
    this.indicatorColor,//指示器颜色
    this.indicatorWeight = 2.0,//指示器高度
    this.indicatorPadding = EdgeInsets.zero,//底部指示器的Padding
    this.indicator,//指示器decoration,例如边框等
    this.indicatorSize,//指示器大小计算方式,TabBarIndicatorSize.label跟文字等宽,TabBarIndicatorSize.tab跟每个tab等宽
    this.labelColor,//选中label颜色
    this.labelStyle,//选中label的Style
    this.labelPadding,//每个label的padding值
    this.unselectedLabelColor,//未选中label颜色
    this.unselectedLabelStyle,//未选中label的Style
    }) : assert(tabs != null),
    assert(isScrollable != null),
    assert(indicator != null || (indicatorWeight != null && indicatorWeight > 0.0)),
    assert(indicator != null || (indicatorPadding != null)),
    super(key: key);

下面我们通过“bottom”属性来添加一个导航栏底部tab按钮组,将要实现的效果如下:


Material组件库中提供了一个TabBar组件,它可以快速生成Tab菜单,下面是上图对应的源码:

class _ScaffoldRouteState extends State<ScaffoldRoute>
    with SingleTickerProviderStateMixin {

  TabController _tabController; //需要定义一个Controller
  List tabs = ["新闻", "历史", "图片"];

  @override
  void initState() {
    super.initState();
    // 创建Controller  
    _tabController = TabController(length: tabs.length, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        ... //省略无关代码
        bottom: TabBar(   //生成Tab菜单
          controller: _tabController,
          tabs: tabs.map((e) => Tab(text: e)).toList()
        ),
      ),
      ... //省略无关代码
  }

上面代码首先创建了一个 TabController ,它是用于控制/监听 Tab 菜单切换。然后通过 TabBar 生成了一个底部菜单栏,TabBar 的 tabs 属性接受一个 Widget 数组,表示每一个 Tab 子菜单,我们可以自定义,也可以像示例中一样直接使用 Tab Widget,它也是 Material 组件库提供的 Material 风格的 Tab 菜单。

Tab Widget有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义Widget,定义如下:

Tab({
  Key key,
  this.text, // 菜单文本
  this.icon, // 菜单图标
  this.child, // 自定义Widget
})

TabBarView


const TabBarView({
    Key key,
    @required this.children, //Tab页内容页组件数组集合
    this.controller, //TabController对象
    this.physics,
    this.dragStartBehavior = DragStartBehavior.start,
  }) : assert(children != null),
       assert(dragStartBehavior != null),
       super(key: key);

通过 TabBar 我们只能生成一个静态的菜单,如果要实现 Tab 页,我们可以通过TabController 去监听 Tab 菜单的切换去切换 Tab 页,代码如:

_tabController.addListener((){  
  switch(_tabController.index){
    case 1: ...;
    case 2: ... ;   
  }
});

如果我们 Tab 页可以滑动切换的话,还需要在滑动过程中更新 TabBar 指示器的偏移。显然,要手动处理这些是很麻烦的,为此,Material 库提供了一个TabBarView 组件,它可以很轻松的配合 TabBar 来实现同步切换和滑动状态同步,示例如下:

Scaffold(
  appBar: AppBar(
    ... //省略无关代码
    bottom: TabBar(
      controller: _tabController,
      tabs: tabs.map((e) => Tab(text: e)).toList()),
  ),
  drawer: new MyDrawer(),
  body: TabBarView(
    controller: _tabController,
    children: tabs.map((e) { //创建3个Tab页
      return Container(
        alignment: Alignment.center,
        child: Text(e, textScaleFactor: 5),
      );
    }).toList(),
  ),
  ... // 省略无关代码  
)

运行后效果如下:


现在,无论是点击导航栏 Tab 菜单还是在页面上左右滑动,Tab 页面都会切换,并且 Tab 菜单的状态和 Tab 页面始终保持同步。可以发现,TabBar 和TabBarView 的 controller 是同一个!正是如此,TabBar 和 TabBarView 正是通过同一个 controller 来实现菜单切换和滑动状态同步的。

抽屉菜单Drawer


Scaffold 的 drawer 和 endDrawer 属性可以分别接受一个 Widget 作为页面的左、右抽屉菜单,如果开发者提供了抽屉菜单,那么当用户手指从屏幕左/右向里滑动时便可打开抽屉菜单。本节开始部分的示例中实现了一个左抽屉菜单MyDrawer,源码如下:

class MyDrawer extends StatelessWidget {
  const MyDrawer({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        // DrawerHeader consumes top MediaQuery padding.
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

抽屉菜单通常将Drawer作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding可以移除抽Drawer内的一些指定空白。

我们也可以使用 UserAccountsDrawerHeader 快速构建了 drawer 的头部布局;头像和背景都是使用的网络图片加载,在菜单 Item 部分我们使用 ListTile 来处理,左边是一个图标右边是文字的样式。
源码如下:

class MyDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: <Widget>[
          UserAccountsDrawerHeader(
            //通常用来设置背景颜色或者背景图片
            decoration: BoxDecoration(
                image: DecorationImage(
                    image: NetworkImage(
                        "http://t2.hddhhn.com/uploads/tu/201612/98/st93.png"))),
            //当前用户的名字
            accountName: Text("flutterDrawer"),
            //当前用户的 Email
            accountEmail: Text('flutterDrawer@163.com'),
            //用来设置当前用户的头像
            currentAccountPicture: CircleAvatar(
              backgroundImage: NetworkImage(
                  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
            ),
            //当 accountName 或者 accountEmail 被点击的时候所触发的回调函数,可以用来显示其他额外的信息
            onDetailsPressed: () {
              print("onDetailsPressed");
            },
            //用来设置当前用户的其他账号的头像(做多显示三个)
            otherAccountsPictures: <Widget>[
              CircleAvatar(
                backgroundImage: AssetImage("images/avatar.png"),
              ),
              CircleAvatar(
                backgroundImage: AssetImage("images/avatar.png"),
              ),
              CircleAvatar(
                backgroundImage: AssetImage("images/avatar.png"),
              ),
            ],
          ),
          ListTile(
            leading: Icon(Icons.refresh),
            title: Text("刷新"),
          ),
          new ListTile(
            leading: new Icon(Icons.help),
            title: new Text("帮助"),
          ),
          new ListTile(
            leading: new Icon(Icons.chat),
            title: new Text("会话"),
          ),
          new ListTile(
            leading: new Icon(Icons.settings),
            title: new Text("设置"),
          ),
        ],
      ),
    );
  }
}

运行效果图如下:


BottomNavigationBar(底部导航条组件)


接下来我们来看下 bottomNavigationBar 底部导航按钮。我们可以通过 Scaffold 的 bottomNavigationBar 属性来设置底部导航,如本节开始示例所示,我们通过 Material 组件库提供的 BottomNavigationBar 和 BottomNavigationBarItem 两个 Widget 来实现 Material 风格的底部导航栏

构造方法:

BottomNavigationBar({
    Key key,
    @required this.items,//List<BottomNavigationBarItem>
    this.onTap,//当底部导航的一个item被点击时,它会调用此方法,并传入当前item的index值,这样就能改变焦点到当前的index上的item了。
    this.currentIndex = 0,//当前再items中被选中的index
    this.elevation = 8.0,
    BottomNavigationBarType type,//底部item类型,fixed自适应,shifting选择放大
    Color fixedColor,//选中颜色
    this.backgroundColor,
    this.iconSize = 24.0,////图标大小
    Color selectedItemColor,
    this.unselectedItemColor,
    this.selectedFontSize = 14.0,
    this.unselectedFontSize = 12.0,
    this.showSelectedLabels = true,
    bool showUnselectedLabels,
  })

来看下BottomNavigationBarItem。

const BottomNavigationBarItem({
    @required this.icon,
    this.title,
    Widget activeIcon,
    this.backgroundColor,
  }) 

示例代码如下:

class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _currentIndex = 0;

  //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
 static GlobalKey<ScaffoldState> _globalKey = new GlobalKey();
  //存储的四个页面,和Fragment一样
  final List<Widget> _children = [
    HomePage(),
    SearchPage(),
    TravelPage(),
    MyPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //设置key
      key: _globalKey,
      appBar: AppBar(
        //导航栏
        title: Text('App Name'),
        leading: Builder(builder: (context) {
          return IconButton(
              //自定义图标
              icon: Icon(
                Icons.dashboard,
                color: Colors.white,
              ),
              onPressed: () {
                //打开抽屉菜单
                //Scaffold.of(context).openDrawer();
                //通过GlobalKey来获取State对象
                _globalKey.currentState.openDrawer();
              });
        }),
        actions: <Widget>[
          //导航栏右侧
          IconButton(
            icon: Icon(Icons.share),
            onPressed: () {},
          )
        ],
      ),
      //抽屉
      drawer: MyDrawer(),

      body: _children[_currentIndex],
      //底部导航
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(
              icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(
              icon: Icon(Icons.school), title: Text('School')),
        ],
        //设置显示的模式
        type: BottomNavigationBarType.fixed,
        //设置当前的索引
        currentIndex: _currentIndex,
        fixedColor: Colors.blue,
        //tabBottom的点击监听
        onTap: _onItemTapped,
      ),
      //悬浮按钮
      floatingActionButton:
          FloatingActionButton(child: Icon(Icons.add), onPressed: _onAdd),
      //悬浮按钮位置
      //floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    );
  }

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

  void _onAdd() {}
}

运行效果图如下:


Material 组件库中提供了一个 BottomAppBar Widget,可以和 FloatingActionButton 配合实现这种"打洞"效果。源码如下:

bottomNavigationBar: BottomAppBar(
  color: Colors.white,
  shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
  child: Row(
    children: [
      IconButton(icon: Icon(Icons.home)),
      SizedBox(), //中间位置空出
      IconButton(icon: Icon(Icons.business)),
    ],
    mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
  ),
)

可以看到,上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于 FloatingActionButton 的位置,上面 FloatingActionButton 的位置为:

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

BottomAppBar的shape 属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,我们也可以自定义外形

示例代码如下:

class ScaffoldRoute extends StatefulWidget {
  @override
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
  int _currentIndex = 0;

  //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
  static GlobalKey<ScaffoldState> _globalKey = new GlobalKey();
  //存储的四个页面,和Fragment一样
  final List<Widget> _children = [
    HomePage(),
    SearchPage(),
    TravelPage(),
    MyPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //设置key
      key: _globalKey,
      appBar: AppBar(
        //导航栏
        title: Text('App Name'),
        leading: Builder(builder: (context) {
          return IconButton(
              //自定义图标
              icon: Icon(
                Icons.dashboard,
                color: Colors.white,
              ),
              onPressed: () {
                //打开抽屉菜单
                //Scaffold.of(context).openDrawer();
                //通过GlobalKey来获取State对象
                _globalKey.currentState.openDrawer();
              });
        }),
        actions: <Widget>[
          //导航栏右侧
          IconButton(
            icon: Icon(Icons.share),
            onPressed: () {},
          )
        ],
      ),
      //抽屉
      drawer: MyDrawer(),
    
      body: _children[_currentIndex],
      //底部导航
       bottomNavigationBar: BottomAppBar(
        color: Colors.white,
        shape: CircularNotchedRectangle(), //底部导航栏打一个圆形的洞
        child: Row(
          children: <Widget>[
            IconButton(icon: Icon(Icons.home), onPressed: null),
            SizedBox(), //中间位置空出
            IconButton(icon: Icon(Icons.business), onPressed: null),
          ],
          //均分底部导航栏横向空间
          mainAxisAlignment: MainAxisAlignment.spaceAround,
        ),
      ),
      //悬浮按钮
      floatingActionButton:
          FloatingActionButton(child: Icon(Icons.add), onPressed: _onAdd),
      //悬浮按钮位置
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    );
  }

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

  void _onAdd() {}
}

运行效果图如下:


FloatingActionButton


FloatingActionButton 是Material设计规范中的一种特殊 Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如本节示例中页面右下角的"➕"号按钮。我们可以通过 Scaffold 的 floatingActionButton 属性来设置一个 FloatingActionButton,同时通过 floatingActionButtonLocation 属性来指定其在页面中悬浮的位置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容