Material组件库提供了丰富多样的组件,本节介绍一些常用的组件,其余的可以自行查看文档或Flutter Gallery。Flutter Gallery是Flutter官方提供的Flutter Demo,源码位于flutter源码中的examples目录下,强烈建议用户将Flutter Gallery示例跑起来,它是一个很全面的Flutter示例应用,是非常好的参考Demo,也是笔者学习Flutter的第一手资料。
一个完整的路由页可能会包含导航栏、抽屉菜单(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是一个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对象。
属性:
/**
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, // 自定义组件样式
})
通过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功能相似,可以自行了解一下。
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是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>[],
);
}
}
我们可以通过Scaffold的bottomNavigationBar属性来设置底部导航。我们通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。
Material组件库中提供了一个BottomAppBar 组件,它可以和FloatingActionButton配合实现这种“打洞”效果,如下:
BottomAppBar (不规则底部工具栏)
BottomAppBar 是 底部工具栏的意思,这个要比BottomNavigationBar widget灵活很多,可以放置文字和图标,当然也可以放置容器。
BottomAppBar的常用属性:
示例:
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示例中就有一个“钻石”形状的示例,感兴趣可以自行查看。