基礎頁面實現
TabBar + TabBarView 實現頁面切換聯動(類似Android tablayout + ViewPage)效果
- 直接上程式碼
List <String>_titles=['湖人','勇士','雄鹿','快船','凱爾特人','馬刺','76人','猛龍'];
TabController _tabController;
///省略部分程式碼
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
///省略部分程式碼
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
@override
void initState() {
super.initState();
//初始化控制器
_tabController = new TabController(length: _titles.length,vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Icon(Icons.menu),
title: buildTabBar(),
//bottom: buildTabBar(),
),
body: TabBarViewLayout()
);
}
Widget buildTabBar() {
return TabBar(
//構造Tab集合
tabs: _titles.map((String title){
return Tab(
text: title,
);
}).toList(),
///省略部分程式碼
controller: _tabController,
);
}
}
// TabBarView Widget
class TabBarViewLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("TabBarViewLayout build.......");
return TabBarView(
controller: _tabController,
children: _titles.map((String title){
return TabPageView(title);
}).toList(),
);
}
}
複製程式碼
- 如果程式碼,可以看到在AppBar這個widget的title屬性中加入TabBar,也就是AppBat的title模組顯示TabBar,也可在AppBar的bottom屬性加入;還需要注意TabBar和TabBarView正是通過同一個controller來實現選單切換和滑動狀態同步的,最終執行結果如下,分被設定tabbar在title 和bottom屬性
下拉重新整理,上拉載入更多實現(RefreshIndicator)
- 下拉重新整理 Flutter SDK中已經提供了一個RefreshIndicator控制元件,所以結合RefreshIndicator控制元件,讓其包裹ListView控制元件,結合滑動監聽ScrollController,並且設定頭部,尾部載入更多等介面,就可以完成一個通用的下拉重新整理,上拉載入更多的通用控制元件。首先來看看RefreshIndicator構造方法
const RefreshIndicator({
Key key,
@required this.child, //包裝一個可滾動widget
this.displacement = 40.0,
@required this.onRefresh, //觸發重新整理呼叫方法
this.color, //指示器顏色
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
})
複製程式碼
- RefreshIndicator包裝一個可滾動widget,這裡使用ListView
@override
Widget build(BuildContext context) {
return RefreshIndicator(
child: ListView.builder(
///保持ListView任何情況都能滾動,解決在RefreshIndicator的相容問題。
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context,index){
return _getItem(index);
},
///根據狀態返回繪製 item 數量
itemCount: _getListCount(),
///滑動監聽
controller: _scrollController,
),
onRefresh: _handleRefresh,
color: Theme.of(context).primaryColor, //指示器顏色
);
}
複製程式碼
- ListView有兩個重要方法設定,一個是itemBuilder構建列表item的每一個頁面,另一個構建item頁面數量itemCount。首先看itemCount方法
///根據配置狀態返回實際列表數量
_getListCount() {
///是否需要頭部
if (widget.isHaveHeader) {
return (items.length > 0) ? items.length + 2 : items.length + 1;
} else {
if (items.length == 0) {
return 1;
}
return (items.length > 0) ? items.length + 1 : items.length;
}
}
複製程式碼
- 該方法中,做了幾種內容型別判斷,如果需要頭部,用Item 0 的 Widget 作為ListView的頭部,列表數量大於0時,因為頭部和底部載入更多選項,需要對列表資料總數+2,如果不需要頭部,在資料獲取為零時,固定返回數量1用於空頁面呈現或者錯誤頁面;如果有資料,加上外部載入更多選項,需要對列表資料總數+1。接著看_getItem()方法,返回對應渲染頁面。
///根據配置狀態返回實際列表渲染Item
_getItem(int index) {
if (!widget.isHaveHeader && index == items.length && items.length != 0) {
return _buildProgressIndicator();
} else if (widget.isHaveHeader && index == _getListCount()-1 && items.length != 0) {
return _buildProgressIndicator();
} else if (widget.isHaveHeader && index == 0 && items.length != 0) {
return widget.headerView();
} else if (!widget.isHaveHeader && items.length == 0) {
///如果不需要頭部,並且資料為0,渲染空頁面
if(isLoading){
return _buildIsLoading();
}else{
return _buildEmpty();
}
} else if(widget.isHaveHeader && items.length == 0){
if(isLoading){
return _buildIsLoading();
}else{
return _buildEmpty();
}
} else {
return widget.renderItem(index, items[widget.isHaveHeader ? index-1 : index]);
}
}
複製程式碼
-
該方法中,如果沒有設定頭部,並且資料不為0,當index等於資料長度時,渲染載入更多頁面(因為index是從0開始);如果設定了頭部頁面,並且資料不為0,當index等於實際渲染長度 - 1時,渲染載入更多頁面(在該方法判斷是否已經載入到底);接著如果設定了頭部widget,並且資料不為0,當index = 0 ,渲染頭部widget;如果沒設定頭部,並且資料為0,如果當前正在重新整理,渲染Loading頁面,否則渲染空頁面或者Error頁面;同理,如果設定頭部,並且資料為0,並且當前正在重新整理,渲染Loading頁面,否則渲染空頁面或者Error頁面;如果不是上面情況,則渲染正常渲染Item,如果這裡有需要,可以直接返回相對位置的index,如果有頭部 index 減一 ,保持不會忽略 index = 0 的資料。
-
接著封裝一個統一網路請求方法,外部請求安裝固定格式的 Map 將資料返回給下拉重新整理上拉載入更多widget,達到通用的目的。
//網路請求獲取資料 isRefresh 是否為下拉重新整理
Future<List> makeHttpRequest(bool isRefresh) async {
if (widget.requestApi is Function) {
Map listObj = new Map<String, dynamic>();
if(isRefresh){
//下拉重新整理
listObj = await widget.requestApi({'pageIndex': 0});
}else{
//上拉載入更多
listObj = await widget.requestApi({'pageIndex': _pageIndex});
}
_pageIndex = listObj['pageIndex'];
_pageTotal = listObj['total'];
return listObj['list'];
} else {
return Future.delayed(Duration(seconds: 2), () {
return [];
});
}
}
複製程式碼
- 基礎東西寫好了,loading 載入動畫這裡直接就使用現成的輪子好了,推薦一個loading庫,flutter_spinkit
- 貼上loading載入程式碼(更多實現細節請看文末demo地址程式碼)
Widget _buildIsLoading() {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height*0.85,
child: new Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
SpinKitCircle(size: 55.0, color: Theme.of(context).primaryColor),
],
),
Padding(
child: Text("正在載入..",
style: TextStyle(color: Colors.black54, fontSize: 15.0)),
padding: EdgeInsets.all(15.0),)
],)
));
}
複製程式碼