flutter实战2:为APP增加上下Tab页

紧接上一篇的有侧边栏APP,这次我们向APP中加入上下Tab页,使之跟趋近主流大部分APP的信息布局套路,等不及看源码的同学可以点击进入我的git仓库下载代码。

Tab关键元素

  • TabController
    这是Tab页的控制器,用于定义Tab标签和内容页的坐标,还可配置标签页的切换动画效果等。

TabController一般放入有状态控件中使用,以适应标签页数量和内容有动态变化的场景,如果标签页在APP中是静态固定的格局,则可以在无状态控件中加入简易版的DefaultTabController以提高运行效率,毕竟无状态控件要比有状态控件更省资源,运行效率更快。

  • TabBar
    Tab页的Title控件,切换Tab页的入口,一般放到AppBar控件下使用,内部有*Title属性。其子元素按水平横向排列布局,如果需要纵向排列,请使用ColumnListView控件包装一下。子元素为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();
  }

然后到TabBarTabBarView中的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),
        ]

最后,我们把定义好的TabBarTabBarView安放到需要的地方去,比如:

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),
            ],
          ),
        ),
      );
  }
}

DefaultTabControllerTabController的用法差异主要在控制器的定义上,TabBarTabBarView的使用方法完全相同,通常上下Tab页的标签分别安放在Scaffold控件的appBarbottomNavigationBar属性上,然后我们把APP填充成下面这个样式:

效果图

代码结构

如上图所示,APP以底部Tab导航栏为主入口,底部每个Tab中又各自有自己的顶部次级Tab页,因此我们把代码结构整理一下:

代码框架
  • _HomePageState是APP的主页面HomePage的子类State
  • 通过Scaffold底部的bottomNavigationBar属性摆放TabBar,搭建Tab页的标签栏
  • 放入Scaffoldbody属性放入TabBarViewTabBarViewchildren是三个外部dart文件定义的控件页面
  • 外部dart文件定义的控件页面分别又有各自风格的Tab标签页
  • Tab页的通用属性可以提前定义到数组List中,在TabBarTabBarView通过遍历提取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(),
      ),
    );
  }

使用获取到的参数

由于StatelessWidgetStatefulWidget的页面构建不同,使用从外部获取到的参数的方式也略有差异,在这里简单总结下。

  • 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文件,把packageandroid:label都改成了APPname2,于是不出意料的悲催了,命令flutter fun报错,无法启动APP,还原配置之后,无法启动APP,即便尝试通过全文搜索APPname1,都按规定格式替换成APPname2,或者逆向改回去,都无法启动APP,此时已是凌晨1点。。。妥妥的血泪史,所以郑重的告诫大家:

不要在项目的各种配置文件中轻易改动项目名称!不要在项目的各种配置文件中轻易改动项目名称!不要在项目的各种配置文件中轻易改动项目名称! 否则你就是下一个在电脑面前捶胸顿足的鱼丸。什么?问我怎么恢复的?当然是托git的福。

感谢大家的支持,请关注我的Flutter圈子,多多投稿,也可以加入flutter 中文社区(官方QQ群:338252156)共同成长,谢谢大家~

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,793评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 前言: 不知不觉一年过去了,这一年过得很累,不确定的产品目标,不确定的技术难度,当然还有不确定的人事变更,都让我的...
    Abson在简书阅读 1,789评论 14 28
  • 世间安得双全法,不负如来不负卿。 “我一直喜欢一个人,所以抱歉了。” 每次遇见男生递给我情书,我总是这样回答,不敢...
    李舒烟阅读 421评论 0 1
  • 众生皆苦,哪有那么多天生就无忧无虑的。你不烦恼柴米油盐,肯定要忧虑诗和远方;你无暇审视精神世界,因为开门几件事,件...
    帝都三少510阅读 1,108评论 0 1