Flutter组件之容器组件Scaffold、TabBar、底部导航

Material组件库提供了丰富多样的组件,本节介绍一些常用的组件,其余的可以自行查看文档或Flutter Gallery。Flutter Gallery是Flutter官方提供的Flutter Demo,源码位于flutter源码中的examples目录下,强烈建议用户将Flutter Gallery示例跑起来,它是一个很全面的Flutter示例应用,是非常好的参考Demo,也是笔者学习Flutter的第一手资料。

1.Scaffold

一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
主要属性:
appBar:显示在界面顶部的一个 AppBar
body:当前界面所显示的主要内容
floatingActionButton: 在 Material 中定义的一个功能按钮。
persistentFooterButtons:固定在下方显示的按钮
drawer:侧边栏控件
bottomNavigationBar:显示在底部的导航栏按钮栏。可以查看文档:Flutter学习之制作底部菜单导航
backgroundColor:背景颜色
resizeToAvoidBottomPadding: 控制界面内容 body是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。
示例:
我们实现一个页面,它包含:

一个导航栏
导航栏右边有一个分享按钮
有一个抽屉菜单
有一个底部导航
右下角有一个悬浮的动作按钮

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      home:
      ScaffoldRoute(),
    );
  }
}

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("导航栏"),
        actions: <Widget>[ //导航栏右侧菜单
          IconButton(icon: Icon(Icons.share), onPressed: () {}),
        ],
      ),
      drawer: new Drawer(), //抽屉
      bottomNavigationBar: BottomNavigationBar( // 底部导航
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('主页')),
          BottomNavigationBarItem(icon: Icon(Icons.book), title: Text('书籍')),
          BottomNavigationBarItem(icon: Icon(Icons.menu), title: Text('我的')),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton( //悬浮按钮
          child: Icon(Icons.add),
          onPressed:_onAdd
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){
  }
}

运行效果如图:
在这里插入图片描述

AppBar

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

AppBar({
  Key key,
  this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  this.title,// 页面标题
  this.actions, // 导航栏右侧菜单
  this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle, //标题是否居中 
  this.backgroundColor,
  ...   //其它属性见源码注释
})

如果给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 组件的State对象。

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按钮组:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
      home:
      ScaffoldRoute(),
    );
  }
}

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

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

  TabController _tabController; //需要定义一个Controller
  List tabs = ["语文", "数学", "英语"];

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
          appBar: AppBar( //导航栏

            bottom: TabBar(
                controller: _tabController,
                tabs: tabs.map((e) => Tab(text: e)).toList()
            ),
            title: Text("导航栏"),
            actions: <Widget>[ //导航栏右侧菜单
              IconButton(icon: Icon(Icons.share), onPressed: () {}),
            ],
          ),
          drawer: new Drawer(), //抽屉
          bottomNavigationBar: BottomNavigationBar( // 底部导航
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('主页')),
              BottomNavigationBarItem(icon: Icon(Icons.book), title: Text('书籍')),
              BottomNavigationBarItem(icon: Icon(Icons.menu), title: Text('我的')),
            ],
            currentIndex: _selectedIndex,
            fixedColor: Colors.blue,
            onTap: _onItemTapped,
          ),
          floatingActionButton: FloatingActionButton( //悬浮按钮
              child: Icon(Icons.add),
              onPressed:_onAdd
          ),
        ),
      ),
    );
  }
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }
  void _onAdd(){
  }
}

运行效果如图:
在这里插入图片描述
上面代码首先创建了一个TabController ,它是用于控制/监听Tab菜单切换的。接下来通过TabBar生成了一个底部菜单栏,TabBar的tabs属性接受一个Widget数组,表示每一个Tab子菜单,我们可以自定义,也可以像示例中一样直接使用Tab 组件,它是Material组件库提供的Material风格的Tab菜单。

Tab组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。Tab组件定义如下:(开发者可以根据实际需求来定制)

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

TabBarView

通过TabBar我们只能生成一个静态的菜单,真正的Tab页还没有实现。由于Tab菜单和Tab页的切换需要同步,我们需要通过TabController去监听Tab菜单的切换去切换Tab页,代码如:

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

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

    return MaterialApp(
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
          appBar: AppBar( //导航栏

            bottom: TabBar(
                controller: _tabController,
                tabs: tabs.map((e) => Tab(text: e)).toList()
            ),
            title: Text("导航栏"),
            actions: <Widget>[ //导航栏右侧菜单
              IconButton(icon: Icon(Icons.share), onPressed: () {}),
            ],
          ),
          body: TabBarView(
            controller: _tabController,
            children: tabs.map((e) { //创建3个Tab页
              return Container(
                alignment: Alignment.center,
                child: Text(e, textScaleFactor: 5),
              );
            }).toList(),
          ),
          drawer: new Drawer(), //抽屉
          bottomNavigationBar: BottomNavigationBar( // 底部导航
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('主页')),
              BottomNavigationBarItem(icon: Icon(Icons.book), title: Text('书籍')),
              BottomNavigationBarItem(icon: Icon(Icons.menu), title: Text('我的')),
            ],
            currentIndex: _selectedIndex,
            fixedColor: Colors.blue,
            onTap: _onItemTapped,
          ),
          floatingActionButton: FloatingActionButton( //悬浮按钮
              child: Icon(Icons.add),
              onPressed:_onAdd
          ),
        ),
      ),
    );

运行后效果如图所示:
在这里插入图片描述
在这里插入图片描述
现在,无论是点击导航栏Tab菜单还是在页面上左右滑动,Tab页面都会切换,并且Tab菜单的状态和Tab页面始终保持同步!那它们是如何实现同步的呢?上例中TabBar和TabBarView的controller是同一个!正是如此,TabBar和TabBarView正是通过同一个controller来实现菜单切换和滑动状态同步的,有关TabController的详细信息,使用时直接查看SDK即可。

另外,Material组件库也提供了一个PageView 组件,它和TabBarView功能相似,可以自行了解一下。

抽屉菜单Drawer

Scaffold的drawer和endDrawer属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。本节开始部分的示例中实现了一个左抽屉菜单MyDrawer,它的源码如下:
Drawer(抽屉组件)可以实现类似抽屉拉出和推入的效果,可以从侧边栏拉出导航面板。在 Scaffold 组件里面传入 drawer 参数可以定义左侧边栏,传入 endDrawer 可以定义右侧边栏。侧边栏默认是隐藏的,我们可以通过手指滑动显示侧边栏,也可以通过点击按钮显示侧边。
示例:

return Scaffold(
    appBar: AppBar(
        title: Text("Flutter App")
    ),
    drawer: Drawer(
        child: Text('左侧侧边栏')
    ),
    endDrawer: Drawer(
        child: Text('右侧侧边栏')
    )
);

DrawerHeader 组件
常见属性:

属性 描述
decoration 设置顶部背景颜色
child 配置子元素
padding 内边距
margin 外边距

示例:

drawer: Drawer(
    child: Column(
        children: <Widget>[
            DrawerHeader(
                decoration: BoxDecoration(
                    color: Colors.yellow,
                    image:DecorationImage(
                        image: NetworkImage("http://image.baidu.com/search/detail?ct=503316480&z=0&ipn=false&word=%E5%A4%B4%E5%83%8F&hs=0&pn=2&spn=0&di=167670&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=3743111107%2C1940472030&os=4020658646%2C126618868&simid=4188567897%2C627433012&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=head&bdtype=11&oriquery=%E5%A4%B4%E5%83%8F&objurl=http%3A%2F%2Fpic4.zhimg.com%2F50%2Fv2-7fece9a613445edb78271216c8c20c6d_hd.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bxtvt_z%26e3BgjpAzdH3FtvijAzdH3Fyxw-n0mmc9lc_z%26e3Bip4s&gsm=2&islist=&querylist="),
                        fit:BoxFit.cover
                    )
                ),
                child: ListView(
                    children: <Widget>[ 
                        Text('我是一个头部')
                    ]
                )
            ), 
            ListTile(
                title: Text("个人中心"),
                leading: CircleAvatar(
                    child: Icon(Icons.people)
                ),
            ),
            Divider(),
            ListTile(
                title: Text("系统设置"), 
                leading: CircleAvatar(
                    child: Icon(Icons.settings)
                ),
            )
        ],
    )
)

UserAccountsDrawerHeader 组件
属性:

属性 描述
decoration 设置顶部背景颜色
accountName 账户名称
accountEmail 账户邮箱
currentAccountPicture 用户头像
otherAccountsPictures 用来设置当前账户其他账户头像

示例:

drawer: Drawer(
    child: Column(
        children: <Widget>[
            UserAccountsDrawerHeader( 
                accountName:Text("xxx"),
                accountEmail:Text("[email protected]"),
                currentAccountPicture: CircleAvatar(
                    backgroundImage:  NetworkImage("http://image.baidu.com/search/detail?ct=503316480&z=0&ipn=false&word=%E5%A4%B4%E5%83%8F&hs=0&pn=3&spn=0&di=87370&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=2029529051%2C139168498&os=689026796%2C559227799&simid=3213374183%2C3891664903&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=head&bdtype=11&oriquery=%E5%A4%B4%E5%83%8F&objurl=http%3A%2F%2F5b0988e595225.cdn.sohucs.com%2Fq_70%2Cc_zoom%2Cw_640%2Fimages%2F20200201%2F43d42a8c4ae64ae8b10e6189d7ab6b1c.jpeg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fooo_z%26e3Bxtvt_z%26e3BgjpAzdH3FtvijAzdH3F14w-89d0d8bn_z%26e3Bip4s&gsm=4&islist=&querylist=")
                    ),
                decoration: BoxDecoration(
                    color: Colors.yellow,
                    image:DecorationImage(
                        image: NetworkImage("https://upload-images.jianshu.io/upload_images/8863827-f214cb00231a4784.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"),
                        fit:BoxFit.cover
                    )
                ),
                otherAccountsPictures: <Widget>[
                    Image.network("https://upload-images.jianshu.io/upload_images/8863827-6f3e06b72ac3f406.png"),
                ]
            ), 
            ListTile(
                title: Text("个人中心"),
                leading: CircleAvatar(
                    child: Icon(Icons.people)
                )
            ), 
            Divider(),
            ListTile(
                title: Text("系统设置"),
                leading: CircleAvatar(
                    child: Icon(Icons.settings) 
                )
            )
        ],
    )
)

抽屉菜单通常将Drawer组件作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding可以移除Drawer默认的一些留白(比如Drawer默认顶部会留和手机状态栏等高的留白),可以尝试传递不同的参数来看看实际效果。抽屉菜单页由顶部和底部组成,顶部由用户头像和昵称组成,底部是一个菜单列表,用ListView实现。

FloatingActionButton

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

/**
    const FloatingActionButton({
    Key key,
    this.child,//按钮显示的内容
    this.tooltip,//长按时显示的提示
    this.foregroundColor,//前景色,影响到文字颜色
    this.backgroundColor,//背景色
    this.heroTag = const _DefaultHeroTag(),//hero效果使用的tag,系统默认会给所有FAB使用同一个tag,方便做动画效果
    this.elevation = 6.0,//未点击时阴影值
    this.highlightElevation = 12.0,//点击下阴影值
    @required this.onPressed,
    this.mini = false,//FloatingActionButton有regular, mini, extended三种类型,默认为false即regular类型,true时按钮变小即mini类型,extended需要通过FloatingActionButton.extended()创建,可以定制显示内容
    this.shape = const CircleBorder(),//定义FAB的shape,设置shape时,默认的elevation将会失效,默认为CircleBorder
    this.clipBehavior = Clip.none,
    this.materialTapTargetSize,
    this.isExtended = false,//是否为”extended”类型
    })
 */

示例:

import 'package:flutter/material.dart';

void main(){ runApp(MyApp());}

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:Scaffold(
        appBar: AppBar(
          title:Text("flutter demo")
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: (){
            print('FloatingActionButton');
          },
        ),
        body:HomeContent() 
      )
    );
  }
}

class HomeContent extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[],
    );
  }
}

底部Tab导航栏

我们可以通过Scaffold的bottomNavigationBar属性来设置底部导航。我们通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。
Material组件库中提供了一个BottomAppBar 组件,它可以和FloatingActionButton配合实现这种“打洞”效果,如下:
BottomAppBar (不规则底部工具栏)
BottomAppBar 是 底部工具栏的意思,这个要比BottomNavigationBar widget灵活很多,可以放置文字和图标,当然也可以放置容器。
BottomAppBar的常用属性:

  • color:这个不用多说,底部工具栏的颜色。
  • shape:设置底栏的形状,一般使用这个都是为了和floatingActionButton融合,所以使用的值都是CircularNotchedRectangle(),有缺口的圆形矩形。
  • child : 里边可以放置大部分Widget,让我们随心所欲的设计底栏。

示例:

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new MaterialApp(
      title: "flutter demo",
      theme: ThemeData(
          primarySwatch: Colors.lightBlue //primarySwatch :现在支持18种主题样本。
      ),
      home: _home(),
    );
  }
}
class _home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return _homeState();
  }
}
class _homeState extends State<_home> {
  List<Widget> _viewList; //创建视图数组
  int _index = 0; //数组索引,通过改变索引值改变视图
  @override
  void initState() {
    super.initState();
    _viewList = List();
    _viewList..add(EachView("HOME"))..add(EachView("CLOCK"));
  }
  _openNewPage() {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (BuildContext context) {
      return EachView("new Pager");
    }));
  }
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
      floatingActionButton: FloatingActionButton(
          onPressed: _openNewPage,
          child: Icon(
            Icons.add,
            color: Colors.white,
          )),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
      //和底栏进行融合
      bottomNavigationBar: BottomAppBar(
        color: Colors.lightBlue, //底部工具栏的颜色。
        shape: CircularNotchedRectangle(),
        //设置底栏的形状,一般使用这个都是为了和floatingActionButton融合,
        // 所以使用的值都是CircularNotchedRectangle(),有缺口的圆形矩形。
        child: Row(
          //里边可以放置大部分Widget,让我们随心所欲的设计底栏
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          mainAxisSize: MainAxisSize.max,
          children: <Widget>[
            IconButton(
              icon: Icon(
                Icons.home,
                color: Colors.white,
              ),
              color: Colors.white,
              onPressed: () {
                setState(() {
                  _index = 0;
                });
              },
            ),
            IconButton(
              icon: Icon(Icons.access_alarms, color: Colors.white),
              color: Colors.white,
              onPressed: () {
                setState(() {
                  _index = 1;
                });
              },
            ),
          ],
        ),
      ),
      body: _viewList[_index],
    );
  }
}
//子页面
//代码中设置了一个内部的_title变量,这个变量是从主页面传递过来的,然后根据传递过来的具体值显示在APP的标题栏和屏幕中间。
class EachView extends StatefulWidget {
  String _title;
  EachView(this._title);
  @override
  _EachViewState createState() => _EachViewState();
}
class _EachViewState extends State<EachView> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget._title)),
      body: Center(child: Text(widget._title)),
    );
  }
}

运行效果如图:
在这里插入图片描述

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

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

所以打洞位置在底部导航栏的正中间。

BottomAppBar的shape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,我们也可以自定义外形,比如,Flutter Gallery示例中就有一个“钻石”形状的示例,感兴趣可以自行查看。

发布了23 篇原创文章 · 获赞 60 · 访问量 6267

猜你喜欢

转载自blog.csdn.net/m0_46369686/article/details/104882487