上一篇中我記錄了Flutter中常用的一些佈局,本篇開始開發基於Flutter的開源中國客戶端了。在本篇部落格中,要實現的是一個App的整體框架,包括頁面底部的Tab導航選單、頁面的側滑選單以及跳轉到新的頁面這幾個功能。希望自己在記錄的同時能溫故知新,同時給初學者一些幫助。
App整體佈局框架搭建
在我們日常生活中經常使用的App比如微信、微博、QQ等,基本上都是使用首頁底部多個Tab可切換頁面,加上可側滑的選單這種佈局方式來組合。基於Flutter的開源中國客戶端也是使用這種佈局組合來實現的App。本篇要實現的頁面效果如下圖所示:
下面一步步來完成這個佈局框架的搭建。
新建專案
在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 {
@override
State<StatefulWidget> createState() => new MyOSCClientState();
}
class MyOSCClientState extends State<MyApp> {
@override
Widget 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.dart
import 'package:flutter/material.dart';
// 資訊列表頁面
class NewsListPage extends StatelessWidget {
@override
Widget 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檔案中已經新增了圖片的路徑,如下圖:
為了達到點選Tab切換不同的頁面的功能,我們需要給CupertinoTabBar元件的onTap引數配置一個方法,該方法有一個index引數,我們將這個index賦值給前面定義的_tabIndex
即可,並將這個賦值操作放到setState中執行,如下程式碼:
onTap: (index) {
// 底部TabItem的點選事件處理,點選時改變當前選擇的Tab的索引值,則頁面會自動重新整理
setState((){
_tabIndex = index;
});
},
複製程式碼
最後放上MyOSCClientState類的build方法程式碼:
@override
Widget 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
引數即可,程式碼如下:
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;
});
},
),
// drawer屬性用於為當前頁面新增一個側滑選單
drawer: new Drawer(
child: new Center(
child: new Text("this is a drawer")
),
),
)
複製程式碼
有了drawer之後的app執行效果如下圖:
實現頁面跳轉邏輯
在Flutter中實現頁面的跳轉非常簡單,使用Navigator的相關API即可,下面我們改造一下NewsListPage頁面,在頁面中間加入一個按鈕,點選按鈕跳轉到詳情頁。
首先我們在pages/
目錄下新建一個NewsDetailPage代表資訊詳情頁,並新增如下程式碼:
import 'package:flutter/material.dart';
class NewsDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("資訊詳情", style: new TextStyle(color: Colors.white)),
iconTheme: new IconThemeData(color: Colors.white)
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text("News Detail Page."),
new RaisedButton(
child: new Text("Back"),
onPressed: () {
Navigator.of(context).pop();
},
)
],
)
),
);
}
}
複製程式碼
上面的程式碼中有如下幾點需要注意:
- 在
build
方法中我們返回的是一個Scaffold元件,而不是像main.dart中那樣返回一個MaterialApp元件,這是因為我們在使用Navigator從資訊列表頁跳轉到詳情頁時,會自動為詳情頁的AppBar左邊新增返回按鈕,如果你在詳情頁還是使用MaterialApp物件,則頁面左上角不會自動新增返回按鈕。 - 上面程式碼中的body部分返回的是一個Center元件,Center中裝的是Column元件,如果你不為Column元件設定
mainAxisAlignment: MainAxisAlignment.center
,則頁面上的元件只會在水平方向居中而不會在垂直方向上居中。 - 使用
Navigator.of(context).pop()
來使頁面返回到上一級。
下面需要修改NewsListPage的程式碼,加入按鈕並完成跳轉到詳情頁的邏輯,程式碼如下:
import 'package:flutter/material.dart';
import 'NewsDetailPage.dart';
// 資訊列表頁面
class NewsListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Center(
child: new RaisedButton(
child: new Text("to detail page"),
onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(builder: (ctx) {
return new NewsDetailPage();
}));
}
)
);
}
}
複製程式碼
使用Navigator.of(context).push()
方法來完成頁面的跳轉,push的引數是一個Route物件,這裡使用了Flutter提供的MaterialPageRoute物件,builder引數是一個方法,返回的就是詳情頁物件。
除了使用上面的方式做頁面的跳轉外,還可以給MaterialApp配置一個route引數,該route引數類似於一個全域性的路由表,根據一個name值導航到對應的頁面,這種方式需要定義一個型別為Map<String, WidgetBuilder>
的變數_route
變數,並在initDate()方法中為這個變數賦值,如下程式碼:
_routes['newsDetail'] = (BuildContext) {
return new NewsDetailPage();
};
複製程式碼
在需要跳轉頁面的時候,呼叫如下方法完成頁面跳轉:
Navigator.of(context).pushNamed("newsDetail");
複製程式碼
如果在頁面跳轉時需要給下一個頁面傳值,可以在下一個頁面的構造方法中接收傳入的值,然後在Navigator呼叫push方法的時候new下一個頁面時,在元件的構造方法中設定傳入的值,具體用法可以參考這裡
原始碼
本篇相關的所有原始碼都在GitHub上demo-flutter-osc專案的v0.1分支。
後記
本篇主要記錄的是基於Flutter的開源中國客戶端整體佈局框架的搭建過程,通過使用Flutter內建的各種Widget,可以很容易的實現這個佈局框架,下一篇準備記錄的是基於Flutter的開源中國客戶端各個靜態頁面的實現。
我的開源專案
- 基於Google Flutter的開源中國客戶端,希望大家給個Star支援一下,原始碼:
- 基於Flutter的俄羅斯方塊小遊戲,希望大家給個Star支援一下,原始碼:
上一篇 | 下一篇 |
---|---|
從0開始寫一個基於Flutter的開源中國客戶端(4) ——Flutter佈局基礎 |
從0開始寫一個基於Flutter的開源中國客戶端(6) ——各個靜態頁面的實現 |