Flutter 滾動控制元件篇-->滾動監聽及控制(ScrollController)

夜夕i發表於2019-10-04

在前面的滾動控制元件篇的文章中,我們提到了controller屬性,他接收一個ScrollController物件。ScrollController的主要作用是控制滾動位置和監聽滾動事件。

本章以ListView為例,展示一下ScrollController的具體用法

ScrollController

ScrollController控制滾動位置和監聽滾動事件。

原始碼示例

建構函式如下:

ScrollController({
  double initialScrollOffset = 0.0, //初始滾動位置
  this.keepScrollOffset = true,//是否儲存滾動位置
  ...
})
複製程式碼

屬性解釋

上面已經表明了具體屬性的意思,這裡介紹一下ScrollController常用的屬性和方法

offset

該屬性表示可滾動元件當前的滾動位置

jumpTo、animateTo

jumpTo(double offset)animateTo(double offset,...):這兩個方法都用於跳轉到指定的位置,它們不同之處在於,後者在跳轉時會執行一個動畫,而前者不會。

滾動監聽

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

例如:

controller.addListener(()=>print(controller.offset))
複製程式碼

這段程式碼就可以列印出當前滾動的位置

程式碼示例:

我們建立一個ListView,當滾動位置發生變化時,我們先列印出當前滾動位置,然後判斷當前位置是否超過1000畫素,如果超過則在螢幕右下角顯示一個“返回頂部”的按鈕,該按鈕點選後可以使ListView恢復到初始位置;如果沒有超過1000畫素,則隱藏“返回頂部”按鈕。

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  ScrollController _controller = new ScrollController();
  bool showToTopBtn = false; //是否顯示“返回到頂部”按鈕

  @override
  void initState() {
    //監聽滾動事件,列印滾動位置
    _controller.addListener(() {
      print(_controller.offset); //列印滾動位置
      if (_controller.offset < 1000 && showToTopBtn) {
        setState(() {
          showToTopBtn = false;
        });
      } else if (_controller.offset >= 1000 && showToTopBtn == false) {
        setState(() {
          showToTopBtn = true;
        });
      }
    });
  }

  @override
  void dispose() {
    //為了避免記憶體洩露,需要呼叫_controller.dispose
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("滾動控制")),
      body: Scrollbar(
        child: ListView.builder(
            itemCount: 100,
            itemExtent: 50.0, //列表項高度固定時,顯式指定高度是一個好習慣(效能消耗小)
            controller: _controller,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text("$index"),
              );
            }),
      ),
      floatingActionButton: !showToTopBtn
          ? null
          : FloatingActionButton(
              child: Icon(Icons.arrow_upward),
              onPressed: () {
                //返回到頂部時執行動畫
                _controller.animateTo(.0,
                    duration: Duration(milliseconds: 200), curve: Curves.ease);
              }),
    );
  }
}
複製程式碼

由於列表項高度為50畫素,當滑動到第20個列表項後,右下角“返回頂部”按鈕會顯示,點選該按鈕,ListView會在返回頂部的過程中執行一個滾動動畫,動畫時間是200毫秒,動畫曲線是Curves.ease

執行效果:

圖片載入失敗!

滾動位置恢復

PageStorage是一個用於儲存頁面(路由)相關資料的元件,它並不會影響子樹的UI外觀。

每次滾動結束,可滾動元件都會將滾動位置offset儲存到PageStorage中,當可滾動元件重新建立時再恢復。

如果ScrollController.keepScrollOffsetfalse,則滾動位置將不會被儲存,可滾動元件重新建立時會使用ScrollController.initialScrollOffset
ScrollController.keepScrollOffsettrue時,可滾動元件在第一次建立時,會滾動到initialScrollOffset處,因為這時還沒有儲存過滾動位置。在接下來的滾動中就會儲存、恢復滾動位置,而initialScrollOffset會被忽略。

當一個路由中包含多個可滾動元件時,如果你發現在進行一些跳轉或切換操作後,滾動位置不能正確恢復,這時你可以通過顯式指定PageStorageKey來分別跟蹤不同的可滾動元件的位置,
如:

ListView(key: PageStorageKey(1), ... );
...
ListView(key: PageStorageKey(2), ... );
複製程式碼

不同的PageStorageKey,需要不同的值,這樣才可以為不同可滾動元件儲存其滾動位置。

注意:一個路由中包含多個可滾動元件時,如果要分別跟蹤它們的滾動位置,並非一定就得給他們分別提供PageStorageKey。這是因為Scrollable本身是一個StatefulWidget,它的狀態中也會儲存當前滾動位置,所以,只要可滾動元件本身沒有被從樹上detach掉,那麼其State就不會銷燬(dispose),滾動位置就不會丟失。只有當Widget發生結構變化,導致可滾動元件的State銷燬或重新構建時才會丟失狀態,這種情況就需要顯式指定PageStorageKey,通過PageStorage來儲存滾動位置,一個典型的場景是在使用TabBarView時,在Tab發生切換時,Tab頁中的可滾動元件的State就會銷燬,這時如果想恢復滾動位置就需要指定PageStorageKey

ScrollPosition

ScrollPosition是用來儲存可滾動元件的滾動位置的。

一個ScrollController物件可以同時被多個可滾動元件使用,ScrollController會為每一個可滾動元件建立一個ScrollPosition物件,這些ScrollPosition儲存在ScrollControllerpositions屬性中。

ScrollPosition是真正儲存滑動位置資訊的物件,offset只是一個便捷屬性:
double get offset => position.pixels;
從上面可以看出offect是來自於position的。

一個ScrollController雖然可以對應多個可滾動元件,但是有一些操作,如讀取滾動位置offset,則需要一對一!
但是我們仍然可以在一對多的情況下,通過其它方法讀取滾動位置,
舉個例子,假設一個ScrollController同時被兩個可滾動元件使用,那麼我們可以通過如下方式分別讀取他們的滾動位置:

...
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
...
複製程式碼

我們可以通過controller.positions.length來確定controller被幾個可滾動元件使用。

ScrollPosition有兩個常用方法:animateTo()jumpTo(),它們是真正來控制跳轉滾動位置的方法,ScrollController的這兩個同名方法,內部最終都會呼叫ScrollPosition的。

需要注意的是,ScrollControlleranimateTo()jumpTo()內部會呼叫所有ScrollPositionanimateTo()jumpTo(),以實現所有和該ScrollController關聯的可滾動元件都滾動到指定的位置。

滾動監聽示例

下面,我們監聽ListView的滾動通知,然後顯示當前滾動進度百分比:

import 'package:flutter/material.dart';

class ScrollNotificationTestRoute extends StatefulWidget {
  @override
  _ScrollNotificationTestRouteState createState() =>
      new _ScrollNotificationTestRouteState();
}

class _ScrollNotificationTestRouteState
    extends State<ScrollNotificationTestRoute> {
  String _progress = "0%"; //儲存進度百分比

  @override
  Widget build(BuildContext context) {
    return Scrollbar( //進度條
      // 監聽滾動通知
      child: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification notification) {
          double progress = notification.metrics.pixels /
              notification.metrics.maxScrollExtent;
          //重新構建
          setState(() {
            _progress = "${(progress * 100).toInt()}%";
          });
          print("BottomEdge: ${notification.metrics.extentAfter == 0}");
          //return true; //放開此行註釋後,進度條將失效
        },
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            ListView.builder(
                itemCount: 100,
                itemExtent: 50.0,
                itemBuilder: (context, index) {
                  return ListTile(title: Text("$index"));
                }
            ),
            CircleAvatar(  //顯示進度百分比
              radius: 30.0,
              child: Text(_progress),
              backgroundColor: Colors.black54,
            )
          ],
        ),
      ),
    );
  }
}
複製程式碼

執行效果:

圖片載入失敗!

Flutter Widget樹中子Widget可以通過傳送通知(Notification)與父(包括祖先)Widget通訊。父級元件可以通過NotificationListener元件來監聽自己關注的通知,這種通訊方式類似於Web開發中瀏覽器的事件冒泡。

可滾動元件在滾動時會傳送ScrollNotification型別的通知,ScrollBar正是通過監聽滾動通知來實現的。通過NotificationListener監聽滾動事件和通過ScrollController有兩個主要的不同:

  • 通過NotificationListener可以在從可滾動元件到widget樹根之間任意位置都能監聽。而ScrollController只能和具體的可滾動元件關聯後才可以。

  • 收到滾動事件後獲得的資訊不同;NotificationListener在收到滾動事件時,通知中會攜帶當前滾動位置和ViewPort的一些資訊,而ScrollController只能獲取當前滾動位置。

在接收到滾動事件時,引數型別為ScrollNotification,它包括一個metrics屬性,它的型別是ScrollMetrics,該屬性包含當前ViewPort及滾動位置等資訊:

  • pixels:當前滾動位置。
  • maxScrollExtent:最大可滾動長度。
  • extentBefore:滑出ViewPort頂部的長度;此示例中相當於頂部滑出螢幕上方的列表長度。
  • extentInside:ViewPort內部長度;此示例中螢幕顯示的列表部分的長度。
  • extentAfter:列表中未滑入ViewPort部分的長度;此示例中列表底部未顯示到螢幕範圍部分的長度。
  • atEdge:是否滑到了可滾動元件的邊界(此示例中相當於列表頂或底部)。

P_P

相關文章