【Flutter學習】基本組件之上下刷新列表(一)


一,概述

  RefreshIndicator是Flutter基於Material設計語言內置的控件,集合了下拉手勢、加載指示器和刷新操作一體,可玩性比FutureBuilder差了一大截,不過大家也用過Material設計語言的其他控件,視覺效果也不賴的。
  要實現拉刷新列表的功能僅僅依靠RefreshIndicator還不行,我們還需要ScrollController對ListView的移動偏移量進行監控。

二,兩個重要的組件

  • RefreshIndicator
    • 構造函數
      /**
       * 下拉刷新組件
       *const RefreshIndicator
          ({
          Key key,
          @required this.child,
          this.displacement: 40.0, //觸發下拉刷新的距離
          @required this.onRefresh, //下拉回調方法,方法需要有async和await關鍵字,沒有await,刷新圖標立馬消失,沒有async,刷新圖標不會消失
          this.color, //進度指示器前景色 默認為系統主題色
          this.backgroundColor, //背景色
          this.notificationPredicate: defaultScrollNotificationPredicate,
          })
       */

    注意

      • RefreshIndicator的子元素必須是一個可以滾動的控件
      • 如果你遇到不符合條件的控件,請將其用可以滾動的控件(如ListView、PageView等)包裝一下
      • onRefresh的回調函數必須是Future<Null>類型
  • ScrollController
    • 構造函數
      ScrollController({
        double initialScrollOffset = 0.0, //初始滾動位置
        this.keepScrollOffset = true,//是否保存滾動位置
        ...
      })
    • 屬性和方法

      • offset:可滾動Widget當前滾動的位置。
      • jumpTo(double offset)、animateTo(double offset,...):這兩個方法用於跳轉到指定的位置,它們不同之處在於,后者在跳轉時會執行一個動畫,而前者不會。
      • 滾動監聽(addListener(listener))

        ScrollController間接繼承自Listenable,我們可以根據ScrollController來監聽滾動事件。如:

        controller.addListener(()=>print(controller.offset))
      • 滾動位置恢復(keepScrollOffset,initialScrollOffset)

        PageStorage是一個用於保存頁面(路由)相關數據的Widget,它並不會影響子樹的UI外觀,其實,PageStorage是一個功能型Widget,它擁有一個存儲桶(bucket),子樹中的Widget可以通過指定不同的PageStorageKey來存儲各自的數據或狀態。

        每次滾動結束,Scrollable Widget都會將滾動位置offset存儲到PageStorage中,當Scrollable Widget 重新創建時再恢復。如果ScrollController.keepScrollOffsetfalse,則滾動位置將不會被存儲,Scrollable Widget重新創建時會使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffsettrue時,Scrollable Widget在第一次創建時,會滾動到initialScrollOffset處,因為這時還沒有存儲過滾動位置。在接下來的滾動中就會存儲、恢復滾動位置,而initialScrollOffset會被忽略。

      • 滾動監聽

        Flutter Widget樹中子Widget可以通過發送通知(Notification)與父(包括祖先)Widget通信。父Widget可以通過NotificationListener Widget來監聽自己關注的通知,這種通信方式類似於Web開發中瀏覽器的事件冒泡,我們在Flutter中沿用“冒泡”這個術語。Scrollable Widget在滾動時會發送ScrollNotification類型的通知,ScrollBar正是通過監聽滾動通知來實現的。通過NotificationListener監聽滾動事件和通過ScrollController有兩個主要的不同:

        1. 通過NotificationListener可以在從Scrollable Widget到Widget樹根之間任意位置都能監聽。而ScrollController只能和具體的Scrollable Widget關聯后才可以。
        2. 收到滾動事件后獲得的信息不同;NotificationListener在收到滾動事件時,通知中會攜帶當前滾動位置和ViewPort的一些信息,而ScrollController只能獲取當前滾動位置。

        NotificationListener

        NotificationListener是一個Widget,模板參數T是想監聽的通知類型,如果省略,則所有類型通知都會被監聽,如果指定特定類型,則只有該類型的通知會被監聽。NotificationListener需要一個onNotification回調函數,用於實現監聽處理邏輯,該回調可以返回一個布爾值,代表是否阻止該事件繼續向上冒泡,如果為true時,則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時,則冒泡繼續。

 

    • ScrollController控制原理

      我們來介紹一下ScrollController的另外三個方法:

      ScrollPosition createScrollPosition(
          ScrollPhysics physics,
          ScrollContext context,
          ScrollPosition oldPosition
      );
      
      void attach(ScrollPosition position) ;
      void detach(ScrollPosition position) ;

      當ScrollController和Scrollable Widget關聯時,Scrollable Widget首先會調用ScrollController的createScrollPosition()方法來創建一個ScrollPosition來存儲滾動位置信息,接着,Scrollable Widget會調用attach()方法,將創建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱為“注冊位置”,只有注冊后animateTo() 和 jumpTo()才可以被調用。當Scrollable Widget銷毀時,會調用ScrollController的detach()方法,將其ScrollPosition對象從ScrollController的positions屬性中移除,這一步稱為“注銷位置”,注銷后animateTo() 和 jumpTo() 將不能再被調用。

      需要注意的是,ScrollController的animateTo() 和 jumpTo()內部會調用所有ScrollPositionanimateTo() 和 jumpTo(),以實現所有和該ScrollController關聯的Scrollable Widget都滾動到指定的位置。

 

三,下拉加載,上拉刷新實現

class Widget_RefreshIndicator_State extends State<Widget_RefreshIndicator_Page> {
  var list = [];
  int page = 0;
  bool isLoading = false;//是否正在請求新數據
  bool showMore = false;//是否顯示底部加載中提示
  bool offState = false;//是否顯示進入頁面時的圓形進度條
 
  ScrollController scrollController = ScrollController();
 
  @override
  void initState() {
    super.initState();
    scrollController.addListener(() {
      if (scrollController.position.pixels ==
          scrollController.position.maxScrollExtent) {
        print('滑動到了最底部${scrollController.position.pixels}');
        setState(() {
          showMore = true;
        });
        getMoreData();
      }
    });
    getListData();
  }
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          appBar: AppBar(
            title: Text("RefreshIndicator"),
          ),
          body: Stack(
            children: <Widget>[
              RefreshIndicator(
                child: ListView.builder(
                  controller: scrollController,
                  itemCount: list.length + 1,//列表長度+底部加載中提示
                  itemBuilder: choiceItemWidget,
                ),
                onRefresh: _onRefresh,
              ),
              Offstage(
                offstage: offState,
                child: Center(
                  child: CircularProgressIndicator(),
                ),
              ),
            ],
          )
      ),
    );
  }
 
  @override
  void dispose() {
    super.dispose();
    //手動停止滑動監聽
    scrollController.dispose();
  }
 
  /**
   * 加載哪個子組件
   */
  Widget choiceItemWidget(BuildContext context, int position) {
    if (position < list.length) {
      return HomeListItem(position, list[position], (position) {
        debugPrint("點擊了第$position條");
      });
    } else if (showMore) {
      return showMoreLoadingWidget();
    }else{
      return null;
    }
  }
 
  /**
   * 加載更多提示組件
   */
  Widget showMoreLoadingWidget() {
    return Container(
      height: 50.0,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text('加載中...', style: TextStyle(fontSize: 16.0),),
        ],
      ),
    );
  }
 
  /**
   * 模擬進入頁面獲取數據
   */
  void getListData() async {
    if (isLoading) {
      return;
    }
    setState(() {
      isLoading = true;
    });
    await Future.delayed(Duration(seconds: 3), () {
      setState(() {
        isLoading = false;
        offState = true;
        list = List.generate(20, (i) {
          return ItemInfo("ListView的一行數據$i");
        });
      });
    });
  }
 
  /**
   * 模擬到底部加載更多數據
   */
  void getMoreData() async {
    if (isLoading) {
      return;
    }
    setState(() {
      isLoading = true;
      page++;
    });
    print('上拉刷新開始,page = $page');
    await Future.delayed(Duration(seconds: 3), () {
      setState(() {
        isLoading = false;
        showMore = false;
        list.addAll(List.generate(3, (i) {
          return ItemInfo("上拉添加ListView的一行數據$i");
        }));
        print('上拉刷新結束,page = $page');
      });
    });
  }
 
  /**
   * 模擬下拉刷新
   */
  Future < void > _onRefresh() async {
    if (isLoading) {
      return;
    }
    setState(() {
      isLoading = true;
      page = 0;
    });
 
    print('下拉刷新開始,page = $page');
 
    await Future.delayed(Duration(seconds: 3), () {
      setState(() {
        isLoading = false;
 
        List tempList = List.generate(3, (i) {
          return ItemInfo("下拉添加ListView的一行數據$i");
        });
        tempList.addAll(list);
        list = tempList;
        print('下拉刷新結束,page = $page');
      });
    });
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM