从 0 开始写一个基于 Flutter 的开源中国客户端(5),带你全面理解 View 的绘制流程
新建项目
在 AndroidStudio 中,通过File
-> New
-> New Flutter Project...
创建一个新的 Flutter 工程。
使用 MaterialApp 和 Scaffold 组件构建首页
在新创建的 Flutter 工程中,删除lib/main.dart
中的代码,并编写下面的代码:
import 'package:flutter/material.dart';
void main() {runApp(new MyApp());}
// MyApp 是一个有状态的组件,因为页面标题,页面内容和页面底部 Tab 都会改变 class MyApp extends StatefulWidget {@overrideState<StatefulWidget> createState() => new MyOSCClientState();}
class MyOSCClientState extends State<MyApp> {@overrideWidget build(BuildContext context) {return new MaterialApp(theme: new ThemeData(// 设置页面的主题色 primaryColor: const Color(0xFF63CA6C)),home: new Scaffold(appBar: new AppBar(// 设置 AppBar 标题 title: new Text("My OSC",// 设置 AppBar 上文本的样式 style: new TextStyle(color: Colors.white)),// 设置 AppBar 上图标的样式 iconTheme: new IconThemeData(color: Colors.white)),body: new Text("MyOSC Client")),);}}
上面的代码中,为 MaterialApp 设置了theme
参数,主要是为了改变页面主题颜色为绿色,在 Scaffold 的appBar
属性中,为title
设置了颜色为白色,如果不设置的话,默认为黑色,appBar
的iconTheme
属性也设置为了白色主题,如果不设置的话,AppBar 上的图标默认为黑色。
编写 4 个页面用于切换显示
在新建的 Flutter 项目的lib/
目录下,新建一个pages/
目录,该目录用于存放 App 中的所有页面,然后分别创建四个.dart 文件:NewsListPage.dart
TweetsListPage.dart
DiscoveryPage.dart
MyInfoPage.dart
,代表 App 中首页底部 4 个 Tab 切换时分别显示的页面,这四个页面暂时就在页面正中间显示一行文本,下面是资讯列表NewsListPage.dart
代码:
// pages/NewsListPage.dartimport 'package:flutter/material.dart';
// 资讯列表页面 class NewsListPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return new Center(child: new Text("NewsListPage"),);}}
其余三个页面代码跟上面的类似,只是换了类名和 Text 组件的文本。
在上一步中,我们的 Scaffold 组件里的 body 属性只是一个 Text 组件,为了加载上面的 4 个页面,需要用一个容器组件将这 4 个页面装起来,然后在点击 Tab 时切换页面,这就用到了我之前的博文里说到的IndexedStack组件了。IndexedStack 中可以有多个子组件,根据索引值来显示其中某个组件而隐藏其余的组件。
在第一步中MyOSCClientState
类中定义两个变量:_tabIndex
和_body
,_tabIndex
表示当前页面底部选中的 Tab 的索引,_body
表示首页 Scaffold 组件的 body 属性值,然后给_tabIndex
和_body
变量赋值,如下代码所示:
// 页面当前选中的 Tab 的索引 int _tabIndex = 0;
// 页面 body 部分组件 var _body = new IndexedStack(children: <Widget>[new NewsListPage(),new TweetsListPage(),new DiscoveryPage(),new MyInfoPage()],index: _tabIndex,);
上面用 IndexedStack 加载了 4 个页面用于切换 Tab 时显示,但是 Tab 我们还没有做出来,Flutter 中为页面添加底部导航 Tab 菜单很简单,已经有很多组件可以用了。
编写页面底部导航 Tab 菜单
给页面添加底部导航 Tab 菜单只需要给 Scaffold 组件添加一个bottomNavigationBar
属性即可,这里的bottomNavigationBar
我们用 Flutter 提供的 CupertinoTabBar 组件。
CupertinoTabBar 是 Flutter 内置的 iOS 风格的选项卡,用于在页面底部显示几个 Tab,要使用 Cupertino 风格的组件,必须先导入头文件,如下代码:
import 'package:flutter/cupertino.dart';
CupertinoTabBar 组件的用法也比较简单,代码如下:
new CupertinoTabBar(items: getBottomNavItems(),currentIndex: _tabIndex,onTap: (index) {// 底部 TabItem 的点击事件处理,点击时改变当前选择的 Tab 的索引值,则页面会自动刷新 setState((){_tabIndex = index;});},)
其中items
是一个List<BottomNavigationBarItem>
对象,currentIndex
表示当前选中的 Tab 的索引值,onTap
是 TabItem 点击事件,上面的代码中,getBottomNavItems()
方法代码如下:
List<BottomNavigationBarItem> getBottomNavItems() {List<BottomNavigationBarItem> list = new List();for (int i = 0; i < 4; i++) {list.add(new BottomNavigationBarItem(icon: getTabIcon(i),title: getTabTitle(i)));}return list;}
// 根据索引值确定 Tab 是选中状态的样式还是非选中状态的样式 TextStyle getTabTextStyle(int curIndex) {if (curIndex == _tabIndex) {return tabTextStyleSelected;}return tabTextStyleNormal;}
// 根据索引值确定 TabItem 的 icon 是选中还是非选中 Image getTabIcon(int curIndex) {if (curIndex == _tabIndex) {return tabImages[curIndex][1];}return tabImages[curIndex][0];}
// 根据索引值返回页面顶部标题 Text getTabTitle(int curIndex) {return new Text(appBarTitles[curIndex],style: getTabTextStyle(curIndex));}
由于 TabItem 是由一个图标和一个文本组件构成,所以这里还需要在 MyOSCClientState 类中定义两个变量tabImages
和appBarTitles
。tabImages
是一个二维数组,表示 TabItem 中的图标(包括选中和未选中状态的图标),appBarTitles
是一个字符串数组,表示每个 TabItem 对应的页面标题,这两个变量的赋值代码如下:
// 页面底部 TabItem 上的图标数组 var tabImages;
// 页面顶部的大标题(也是 TabItem 上的文本)var appBarTitles = ['资讯', '动弹', '发现', '我的'];
// 数据初始化,包括 TabIcon 数据和页面内容数据 void initData() {if (tabImages == null) {tabImages = [[getTabImage('images/ic_nav_news_normal.png'),getTabImage('images/ic_nav_news_actived.png')],[getTabImage('images/ic_nav_tweet_normal.png'),getTabImage('images/ic_nav_tweet_actived.png')],[getTabImage('images/ic_nav_discover_normal.png'),getTabImage('images/ic_nav_discover_actived.png')],[getTabImage('images/ic_nav_my_normal.png'),getTabImage('images/ic_nav_my_pressed.png')]];}}
// 传入图片路径,返回一个 Image 组件 Image getTabImage(path) {return new Image.asset(path, width: 20.0, height:
20.0);}
上面的代码中需要注意的是 Image 组件,要使用image/
目录下的图片,必须确保项目根目录下的 pubspec.yaml 文件中已经添加了图片的路径,如下图:
如果没有上面的 assets 配置,直接加载图片是会报错的。
为了达到点击 Tab 切换不同的页面的功能,我们需要给 CupertinoTabBar 组件的 onTap 参数配置一个方法,该方法有一个 index 参数,我们将这个 index 赋值给前面定义的_tabIndex
即可,并将这个赋值操作放到 setState 中执行,如下代码:
onTap: (index) {// 底部 TabItem 的点击事件处理,点击时改变当前选择的 Tab 的索引值,则页面会自动刷新 setState((){_tabIndex = index;});},
最后放上 MyOSCClientState 类的 build 方法代码:
@overrideWidget build(BuildContext context) {initData();return new MaterialApp(theme: new ThemeData(// 设置页面的主题色 primaryColor: const Color(0xFF63CA6C)),home: new Scaffold(appBar: new AppBar(// 设置 AppBar 标题 title: new Text(appBarTitles[_tabIndex],// 设置 AppBar 上文本的样式 style: new TextStyle(color: Colors.white)),// 设置 AppBar 上图标的样式 iconTheme: new IconThemeData(color: Colors.white)),body: _body,// bottomNavigationBar 属性为页面底部添加导航的 Tab,CupertinoTabBar 是 Flutter 提供的一个 iOS 风格的底部导航栏组件 bottomNavigationBar: new CupertinoTabBar(items: getBottomNavItems(),currentIndex: _tabIndex,onTap: (index) {// 底部 TabItem 的点击事件处理,点击时改变当前选择的 Tab 的索引值,则页面会自动刷新 setState((){_tabIndex = index;});},)),);}
在上面的代码中,body
属性是_body
变量,而_body
变量是个 IndexedStack 对象,IndexedStack 对象的index
值是_tabIndex
,所以当我们在 setState 中改变了_tabIndex
后,IndexedStack 就会自动切换显示子组件了,也就达到了切换页面的目的。
上面的代码运行在模拟器中如下图所示:
给首页加上侧滑菜单
侧滑菜单在 Flutter 中已有相关组件,所以为首页加上侧滑菜单的方法很简单:给 Scaffold 组件传个drawer
参数即可,代码如下:
评论