【Flutter 元件集錄】NotificationListener| 8月更文挑戰

張風捷特烈發表於2021-08-01

1. 引子

在研究 ScrollView 原始碼時,有個很有意思的收穫。這裡作為引子,來引入 NotificationListener 元件。下面是 ScrollView#build 原始碼中的一部分,可以看出,當 keyboardDismissBehavioronDrag 時,所構建的元件上層會巢狀一個 NotificationListener 元件,並在 onNotification 中進行邏輯性處理。

其中 ScrollViewKeyboardDismissBehavior 是隻有兩個元素的列舉。

enum ScrollViewKeyboardDismissBehavior {
  manual,
  onDrag,
}
複製程式碼

ListView 繼承自ScrollView ,構造中的 keyboardDismissBehavior 引數,會為 ScrollView 中定義的該成員進行初始化。測試的核心程式碼如下:

manualonDrag 的效果如下:當前 鍵盤彈出時,如果為 manual ,列表滑動過程中鍵盤不會主動隱藏 。為 onDrag 時,滑動列表時,鍵盤會主動隱藏

manualonDrag

通過原始碼中的一個小細節處理,我們能夠清楚地認識到 NotificationListener 價值。它可以監聽滑動的過程,回撥出相關的資料讓使用者進行邏輯處理


2. 認識 NotificationListener

首先 NotificationListener 是一個 StatelessWidget ,接受一個 Notification 族 泛型,構造方法中必須傳入一個 child 元件,可以設定 onNotification 的監聽。

onNotification 成員的型別為 NotificationListenerCallback<T> ,可以看出它是一個函式型別,返回 bool 值。入參為 T 泛型物件,且必須是 Notification 子類 。也就是說,該函式會回撥出一個資料,並且返回一個用於控制某個邏輯的標識。

---->[NotificationListener#onNotification 宣告]----
final NotificationListenerCallback<T>? onNotification;

typedef NotificationListenerCallback<T extends Notification> = 
                                               bool Function(T notification);
複製程式碼

既然作為一個 StatelessWidget,那最重要的當屬 build 方法。 但從原始碼中可以看出,該元件直接返回使用者傳入的 child ,也就是說,它並不關心元件的構建邏輯。


最後,該類中還有一個私有方法 _dispatch ,該方法中需要傳入 Notification 物件,可以看出,這裡是使用者傳入的 onNotification 方法觸發場合。


3.認識 Notification

上面涉及到了很多處 Notification ,如果不瞭解這個類,那麼很難對 NotificationListener 有全面的認識。Notification 是一個 抽象類 ,它沒有繼承任何類。其中有兩個普通方法 visitAncestordispatch


既然 Notification 是抽象類,那麼並不能直接構造物件,所以 Flutter 框架中自然要提供相關的實現類,如下是 Notification 的眾多實現類,包括引子中的 ScrollUpdateNotification 。這樣我們就知道能監聽到哪些 Notification


4.認識 NotificationListener 的使用

比如下面我們通過 NotificationListener 監聽 ScrollUpdateNotification ,這樣滑動時_onNotification 回撥就可以回撥出 notification 滑動資料。我們就可以根據這個物件進行相關邏輯處理。

class ListViewDemo extends StatelessWidget {
  const ListViewDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollUpdateNotification>(
      onNotification: _onNotification,
      child: ListView(
          children: List.generate(
              60,
              (index) => ItemBox(index: index, )).toList()),
    );
  }

  bool _onNotification(ScrollUpdateNotification notification) {
    print('====dragDetails:${notification.dragDetails}'
        '====pixels:${notification.metrics.pixels}');
    return false;
  }
}
複製程式碼

測試條目單體如下,使用 ItemBox 元件進行展示。

class ItemBox extends StatelessWidget {
  final int index;

  const ItemBox({Key? key, required this.index}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      decoration: BoxDecoration(
          border: Border(
              bottom: BorderSide(
        width: 1 / window.devicePixelRatio,
      ))),
      height: 56,
      child: Text(
        '第 $index 個',
        style: TextStyle(fontSize: 20),
      ),
    );
  }
}
複製程式碼

我們可以監聽任意的 Notification 型別,比如下面的 OverscrollNotification,這個監聽將會在列表滑動到最頂端或最底端時被觸發,在回撥的資料中可以得到越界的尺寸 overscroll

class ListViewDemo extends StatelessWidget {
  const ListViewDemo({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return NotificationListener<OverscrollNotification>(
      onNotification: _onNotification,
      child: ListView(
          children: List.generate(
              60,
              (index) => ItemBox( index: index,)).toList()),
    );
  }
  bool _onNotification(OverscrollNotification notification) {
    print('====dragDetails:${notification.dragDetails}'
   '====pixels:${notification.metrics.pixels}'
   '=====overscroll:${notification.overscroll}');
    return false;
  }
}
複製程式碼

除了對某一種 Notification 監聽,我們也可以通過父級來監聽多個 Notification,下面是監聽頂層抽象 Notification 類的程式碼。簡單上滑後日志如下,可以看出,這樣能夠同時監聽到多種型別的 Notification 通知,我們可以通過型別判斷來進行區分。

class ListViewDemo extends StatelessWidget {
  const ListViewDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return NotificationListener<Notification>(
      onNotification: _onNotification,
      child: ListView(
          children: List.generate(
              60,
              (index) => ItemBox( index: index,)).toList()),
    );
  }

  bool _onNotification(Notification notification) {
    print('====Notification type:${notification.runtimeType}======');
    return false;
  }
}
複製程式碼

5. 原始碼中對 NotificationListener 的使用

最經典的當屬 Scrollbar 原始碼中對 NotificationListener 的使用,它監聽 ScrollNotification 的五種通知,通過 _handleScrollNotification 進行處理。

這樣只要在 ListView 外層巢狀一個 Scrollbar ,在滑動過程中右側就可以出現指示器。

Scrollbar(
  child: ListView(
      children: List.generate(
          60,
          (index) => ItemBox(index: index,)).toList()),
),
複製程式碼


另外 RefreshIndicator 元件內部也是基於監聽 ScrollNotificationOverscrollIndicatorNotification 通知進行實現的。


6. NotificationListener 監聽中返回值的作用

從原始碼中可以看出,當返回 false 則表示通知可以繼續向上層節點分發。反之也就意味著通知被截斷。

比如下面程式碼,將 NotificationListener 放在 Scrollbar 下方,監聽時返回 true。這樣 ListView 的滑動事件向上分發時,到 NotificationListener 時,被攔截,就無法再向上傳到 Scrollbar 中的監聽。也就是說 Scrollbar 不起作用了。


Flutter 的滑動體系中通過 Notification 的分發與監聽,讓我們可以在任何地方去監聽元件的滑動。這樣滑動事件的得到了極大地解耦。至於滑動通知的具體流程,不是一言半語能夠介紹完的。作為普通的使用者,瞭解到這樣就已足夠。我的第四本小冊 《Flutter 滑動探索 - 珠聯璧合》 中將會全面分析 Flutter 滑動體系的原始碼實現,敬請期待。

相關文章