Flutter 下拉重新整理上拉載入更多

maoqitian發表於2019-12-08

image

基礎頁面實現

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屬性
    image
    image

下拉重新整理,上拉載入更多實現(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),)
                ],)
    ));
  }
複製程式碼

最終demo 效果

Flutter 下拉重新整理上拉載入更多Flutter 下拉重新整理上拉載入更多Flutter 下拉重新整理上拉載入更多

Demo 地址

About me

blog:

mail:

相關文章