紧接上一篇的有侧边栏APP,这次我们向APP中加入上下Tab页,使之跟趋近主流大部分APP的信息布局套路,等不及看源码的同学可以点击进入我的git仓库下载代码。
Tab关键元素
-
TabController
这是Tab
页的控制器,用于定义Tab
标签和内容页的坐标,还可配置标签页的切换动画效果等。
TabController一般放入有状态控件中使用,以适应标签页数量和内容有动态变化的场景,如果标签页在APP中是静态固定的格局,则可以在无状态控件中加入简易版的DefaultTabController以提高运行效率,毕竟无状态控件要比有状态控件更省资源,运行效率更快。
TabBar
Tab
页的Title
控件,切换Tab
页的入口,一般放到AppBar
控件下使用,内部有*Title属性。其子元素按水平横向排列布局,如果需要纵向排列,请使用Column
或ListView
控件包装一下。子元素为Tab
类型的数组。TabBarView
Tab
页的内容容器,其内放置Tab
页的主体内容。子元素可以是多个各种类型的控件。
Tab使用方法
有状态控件搭配TabController
Tab
页的切换搭配了动画,因此到State
类上附加一个SingleTickerProviderStateMixin
:
//定义有状态控件
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => new _HomePageState();
}
//用于使用到了一点点的动画效果,因此加入了SingleTickerProviderStateMixin
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin{
...
}
然后到有状态控件的子类State
中初始化控制器TabController
:
@override
void initState() {
super.initState();
_tabController = new TabController(
vsync: this, //动画效果的异步处理,默认格式,背下来即可
length: 3 //需要控制的Tab页数量
);
}
//当整个页面dispose时,记得把控制器也dispose掉,释放内存
@override
void dispose() {
_tabController .dispose();
super.dispose();
}
然后到TabBar
和TabBarView
中的controller属性中调用控制器_tabController
//标签页标题
new TabBar(
tabs: [ //注意TabBar的子元素为Tab类型的数组
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
]
//标签页内容区域
new TabBarView(
children: [
new Icon(Icons.directions_car),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
]
最后,我们把定义好的TabBar
和TabBarView
安放到需要的地方去,比如:
new Scaffold(
appBar: new AppBar(
backgroundColor: Colors.deepOrange,
title: new Text('title'),
),
....
body: new TabBarView( //TabBarView呈现内容,因此放到Scaffold的body中
controller: _bottomNavigation, //配置控制器
children: [ //注意顺序与TabBar保持一直
new News(data: '参数值'), //上一篇定义好的子页面
new TabPage2(),
new TabPage3(),
]
),
bottomNavigationBar: new Material( //为了适配主题风格,包一层Material实现风格套用
color: Colors.deepOrange, //底部导航栏主题颜色
child: new TabBar( //TabBar导航标签,底部导航放到Scaffold的bottomNavigationBar中
controller: _bottomNavigation, //配置控制器
tabs: _bottomTabs,
indicatorColor: Colors.white, //tab标签的下划线颜色
),
)
);
无状态控件搭配DefaultTabController
DefaultTabController
要简单很多,由于应用在无状态控件中,使用DefaultTabController
包裹需要用到Tab
的页面即可:
class TabPage3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
backgroundColor: Colors.orangeAccent,
title: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
indicatorColor: Colors.white,
),
),
body: new TabBarView(
children: [
new Icon(Icons.directions_car),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
);
}
}
DefaultTabController
和TabController
的用法差异主要在控制器的定义上,TabBar
和TabBarView
的使用方法完全相同,通常上下Tab
页的标签分别安放在Scaffold
控件的appBar和bottomNavigationBar属性上,然后我们把APP填充成下面这个样式:
代码结构
如上图所示,APP以底部Tab
导航栏为主入口,底部每个Tab
中又各自有自己的顶部次级Tab
页,因此我们把代码结构整理一下:
-
_HomePageState
是APP的主页面HomePage
的子类State
- 通过
Scaffold
底部的bottomNavigationBar属性摆放TabBar
,搭建Tab
页的标签栏 - 放入
Scaffold
的body属性放入TabBarView
,TabBarView
的children是三个外部dart文件定义的控件页面 - 外部dart文件定义的控件页面分别又有各自风格的
Tab
标签页 -
Tab
页的通用属性可以提前定义到数组List
中,在TabBar
和TabBarView
通过遍历提取List
的值创建子元素可以提高代码的维护效率:
//在`StatefulWidget`控件中,可通过修改下面的数组,实现Tab页的动态加载
final List<Tab> myTabs = <Tab>[
new Tab(text: 'Tab1'),
new Tab(text: 'Tab2'),
new Tab(text: 'Tab3'),
new Tab(text: 'Tab4'),
new Tab(text: 'Tab5'),
new Tab(text: 'Tab6'),
new Tab(text: 'Tab7'),
new Tab(text: 'Tab8'),
new Tab(text: 'Tab9'),
new Tab(text: 'Tab10'),
new Tab(text: 'Tab11'),
];
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
backgroundColor: Colors.orangeAccent,
title: new TabBar(
controller: _tabController,
tabs: myTabs, //使用Tab类型的数组呈现Tab标签
indicatorColor: Colors.white,
isScrollable: true,
),
),
body: new TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) { //遍历List<Tab>类型的对象myTabs并提取其属性值作为子控件的内容
return new Center(child: new Text(tab.text+' '+widget.data)); //使用参数值
}).toList(),
),
);
}
使用获取到的参数
由于StatelessWidget
和StatefulWidget
的页面构建不同,使用从外部获取到的参数的方式也略有差异,在这里简单总结下。
-
StatelessWidget
的获参和用参方式
定义StatelessWidget
控件时,添加一个final
类型的变量如pageText
,用于为参数值预留空间,并在构造函数中加入参数值:
class SidebarPage extends StatelessWidget {
final String pageText; //定义一个常量,用于保存跳转进来获取到的参数
SidebarPage(this.pageText); //构造函数,获取参数
...
}
使用参数时直接引用即可:
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text(pageText),), //将参数当作页面标题
body: new Center(
child: new Text('pageText'),
),
);
}
从外部传入参数时,直接向构造函数中填入参数值即可:
Navigator.of(context).push(new MaterialPageRoute(builder:
(BuildContext context) => new SidebarPage('First Page'))); //在new方法时调用控件的构造函数传入参数值
-
StatefulWidget
的获参和用参方式
相比StatelessWidget
略复杂。定义构造函数时需要默认声明key:
class TabPage1 extends StatefulWidget {
const TabPage1({ Key key , this.data}) : super(key: key); //构造函数中增加参数
final String data; //为参数分配空间
@override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
使用时,由于在State
子类中实现具体的页面内容,因此State
子类使用父类TabPage1
的参数时需要在参数名前增加一个widget关键字:
class _MyTabbedPageState extends State<TabPage1> {
...
new Center(child: new Text(tab.text+' '+widget.data)); //使用参数值,需在参数名前增加widget前缀
...
}
从外部传入参数时,需要声明参数名:
new TabBarView
controller: _bottomNavigation,
children: [
new TabPage1(data: '参数值'), //new方法调用构造函数时,还需要声明参数名称
new TabPage2(),
new TabPage3(),
]
)
好勒,今天就讲到这里,大家去下载我的git源码试试效果吧,代码中有附加的注释,对一些控件属性的特性也有单独的描述,相信看完源码之后,大家也可以自行实现效果了。
顺便分享一个雷区,由于当初创建这个项目时,我使用命令flutter create [APPname1]的方式创建了这个项目,但我发现这个APPname1(代称,并非真实名称)不好听,想把项目改名为APPname2,于是参考之前写的安卓怎么打包?,把项目文件夹改名为APPname2,并非常任性的把项目目录下的android\app\src\main\AndroidManifest.xml文件,把package和android:label都改成了APPname2,于是不出意料的悲催了,命令flutter fun报错,无法启动APP,还原配置之后,无法启动APP,即便尝试通过全文搜索APPname1,都按规定格式替换成APPname2,或者逆向改回去,都无法启动APP,此时已是凌晨1点。。。妥妥的血泪史,所以郑重的告诫大家:
不要在项目的各种配置文件中轻易改动项目名称!不要在项目的各种配置文件中轻易改动项目名称!不要在项目的各种配置文件中轻易改动项目名称! 否则你就是下一个在电脑面前捶胸顿足的鱼丸。什么?问我怎么恢复的?当然是托git的福。
感谢大家的支持,请关注我的Flutter圈子,多多投稿,也可以加入flutter 中文社区(官方QQ群:338252156)共同成长,谢谢大家~