【Flutter】可滾動組件之滾動控制和監聽


前言

可以用ScrollController來控制可滾動組件的滾動位置。

接口描述

ScrollController({
    // 初始滾動位置
    double initialScrollOffset = 0.0,
    // 是否保持滾動位置
    this.keepScrollOffset = true,
    this.debugLabel,
  })

代碼示例

// ScrollController

// 可以用ScrollController來控制可滾動組件的滾動位置。

import 'package:flutter/material.dart';

class ScrollControllerTest extends StatefulWidget{
  @override
  ScrollControllerTestState createState() => ScrollControllerTestState();
}

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

  @override
  void initState(){
    super.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();
  }

  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('滾動控制1'),
      ),
      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,
            // 返回頂部的過程中執行一個滾動動畫,動畫時間是200毫秒,動畫曲線是Curves.ease
            duration: Duration(milliseconds: 200),
            curve: Curves.ease,
          );
        },
      ),
    );
  }

}



// 滾動監聽
class ScrollNotificationTest extends StatefulWidget{
  @override
  _ScrollNotificationTestState createState() => _ScrollNotificationTestState();
}

class _ScrollNotificationTestState extends State<ScrollNotificationTest>{
  //保持進度百分比
  String _progress = '0%';

  Widget build(BuildContext context){
    return Scaffold(
      appBar: AppBar(
        title: Text('滾動控制2'),
      ),
      // 進度條
      body: Scrollbar(
        // 監聽滾動通知
        child: NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification){
            // pixels:當前滾動位置;maxScrollExtent:最大可滾動長度
            double progress = notification.metrics.pixels /
                notification.metrics.maxScrollExtent;
            // 重新構建
            setState(() {
              _progress = '${(progress * 100).toInt()}%';
            });
            // extentBefore:滑出ViewPort頂部的長度;此示例中相當於頂部滑出屏幕上方的列表長度。
            // extentInside:ViewPort內部長度;此示例中屏幕顯示的列表部分的長度。
            // extentAfter:列表中未滑入ViewPort部分的長度;此示例中列表底部未顯示到屏幕范圍部分的長度。
            // atEdge:是否滑到了可滾動組件的邊界(此示例中相當於列表頂或底部)。
            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,
              )
            ],
          ),
        ),
      ),
    );
  }
}

總結

滾動位置恢復

PageStorage是一個用於保存頁面(路由)相關數據的組件,它並不會影響子樹的UI外觀,其實,PageStorage是一個功能型組件,它擁有一個存儲桶(bucket),子樹中的Widget可以通過指定不同的PageStorageKey來存儲各自的數據或狀態。
每次滾動結束,可滾動組件都會將滾動位置offset存儲到PageStorage中,當可滾動組件重新創建時再恢復。如果ScrollController.keepScrollOffset為false,則滾動位置將不會被存儲,可滾動組件重新創建時會使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffset為true時,可滾動組件在第一次創建時,會滾動到initialScrollOffset處,因為這時還沒有存儲過滾動位置。在接下來的滾動中就會存儲、恢復滾動位置,而initialScrollOffset會被忽略。
當一個路由中包含多個可滾動組件時,如果你發現在進行一些跳轉或切換操作后,滾動位置不能正確恢復,這時你可以通過顯式指定PageStorageKey來分別跟蹤不同的可滾動組件的位置。

ScrollPosition

ScrollPosition是用來保存可滾動組件的滾動位置的。一個ScrollController對象可以同時被多個可滾動組件使用,ScrollController會為每一個可滾動組件創建一個ScrollPosition對象,這些ScrollPosition保存在ScrollController的positions屬性中(List )。ScrollPosition是真正保存滑動位置信息的對象,offset只是一個便捷屬性。
一個ScrollController雖然可以對應多個可滾動組件,但是有一些操作,如讀取滾動位置offset,則需要一對一!但是我們仍然可以在一對多的情況下,通過其它方法讀取滾動位置。ScrollPosition有兩個常用方法:animateTo() 和 jumpTo(),它們是真正來控制跳轉滾動位置的方法,ScrollController的這兩個同名方法,內部最終都會調用ScrollPosition的。

ScrollController控制原理

ScrollController的另外三個方法:

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

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

滾動監聽

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

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


免責聲明!

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



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