OverscrollNotification不起效果引起的Flutter感悟分享

本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

問題

先說下問題與解決思路,以及解決方案。
當(dāng)我們想要監(jiān)聽一個(gè)widget的滑動(dòng)狀態(tài)時(shí),可以使用:NotificationListener
在我目前空余時(shí)間寫的一個(gè)flutter項(xiàng)目中,有一個(gè)十分復(fù)雜的組件,需要用到這東西。
要實(shí)現(xiàn)下面這個(gè)功能。

Feb-25-2019 00-37-43.gif

這個(gè)UI由哪些功能點(diǎn)

  • 當(dāng)listview的第一個(gè)條目顯示出來的時(shí)候,此時(shí)繼續(xù)下拉,整個(gè)listview下移
  • 當(dāng)listview處于最底部時(shí),向上拖拽時(shí),整個(gè)listview上移
  • 當(dāng)手離開屏幕時(shí),如果listview的最高高度處于屏幕高度二分之一以上,整個(gè)listview自動(dòng)滾動(dòng)到最頂部
  • 當(dāng)手離開屏幕時(shí),如果listview的最高高度處于屏幕高度二分之一以下,整個(gè)listview自動(dòng)滾動(dòng)到最底部

這篇博客呢,講的就是關(guān)于功能點(diǎn)一的。當(dāng)listview的第一個(gè)條目顯示出來的時(shí)候,此時(shí)繼續(xù)下拉。我要處理這個(gè)情況的UI。

由這個(gè)問題,引發(fā)的解決問題的思路,以及關(guān)于學(xué)習(xí)新姿勢(shì)的一些思考與感悟。

PS: 為了達(dá)到完美的效果,這個(gè)需求,我搞了一周~~

NotificationListener的使用


final GlobalKey _key = GlobalKey();
  @override
  Widget build(BuildContext context) { 
    final Widget child = NotificationListener<ScrollStartNotification>(
      key: _key,
      child: NotificationListener<ScrollUpdateNotification>(
        child: NotificationListener<OverscrollNotification>(
          child: NotificationListener<ScrollEndNotification>(
            child: widget.child,
            onNotification: (ScrollEndNotification notification) { 
              return false;
            },
          ),
          onNotification: (OverscrollNotification notification) { 
            return false;
          },
        ),
        onNotification: (ScrollUpdateNotification notification) {
          return false;
        },
      ),
      onNotification: (ScrollStartNotification scrollUpdateNotification) { 
        return false;
      },
    );

    return child;
  }

其中,

  • ScrollStartNotification 組件開始滑動(dòng)
  • ScrollUpdateNotification 組件位置發(fā)生改變
  • OverscrollNotification 表示窗口小組件未更改它的滾動(dòng)位置,因?yàn)楦臅?huì)導(dǎo)致滾動(dòng)位置超出其滾動(dòng)范圍
  • ScrollEndNotification 組件已經(jīng)停止?jié)L動(dòng)

Demo


body: SafeArea(
            child: NotificationListener<ScrollStartNotification>(
          child: NotificationListener<OverscrollNotification>(
            child: ListView.builder(
                itemBuilder: (BuildContext context, int index) {
                  return Text('data=$index');
                },
                itemCount: 100),
            onNotification: (OverscrollNotification notification) {
              print('OverscrollNotification');
            },
          ),
          onNotification: (ScrollStartNotification notification) {
            print('ScrollStartNotification');
          },
        ))

在Android中效果

在Android中效果

可以看到剛開始下拉的時(shí)候,回調(diào)的是ScrollStartNotificationonNotification方法,之后都是OverscrollNotification。

在ios中效果

在ios中效果

可以看到OverscrollNotification不會(huì)被調(diào)用,調(diào)用的是ScrollStartNotification

在我的一些復(fù)雜UI效果中,需要在OverscrollNotification回調(diào)中做一些事情。
當(dāng)ScrollView滾動(dòng)到頂部時(shí),繼續(xù)下拉時(shí)。在Android平臺(tái)中,OverscrollNotification會(huì)被調(diào)用;在iOS平臺(tái)的真機(jī)中,OverscrollNotification不會(huì)被調(diào)用,調(diào)用的是ScrollStartNotification。這就造成了平臺(tái)的不一致性。我也嘗試了Google一下,但是…我看到這個(gè)問題的時(shí)候,問題還沒解決。后來我就解決了,然后給了他回答。這個(gè)后面再說。

屏幕快照 2019-02-24 下午9.23.50.png

問題OverscrollNotification在Android中正常調(diào)用;在iOS的真機(jī)中,無法調(diào)用。

定位原因

分析NotificationListener的onNotification調(diào)用棧。

  • Step 1 翻源碼

ListView是繼承自ScrollView的。我們跟著ScrollView的build方法,一步步向上級(jí)查詢,可以看到scroll_activity.dart的下面幾個(gè)跟OverscrollNotification相關(guān)的方法:

scroll_activity

這就明了多了。

  • Step 2 翻源碼

繼續(xù)上溯,進(jìn)入到scroll_position.dart,看到OverscrollNotification被實(shí)際調(diào)用的方法:

scroll_postion.png

  • Step 3OverscrollNotification能否被調(diào)用的判斷位置
OverscrollNotification能否被調(diào)用
  • Step 4 分析applyBoundaryConditions方法

@protected
  double applyBoundaryConditions(double value) {
    final double result = physics.applyBoundaryConditions(this, value);//這里physics來控制返回值
    assert(() {
      final double delta = value - pixels;
      if (result.abs() > delta.abs()) {
        throw FlutterError(
          '${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
          'The method was called to consider a change from $pixels to $value, which is a '
          'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
          '${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
          'The applyBoundaryConditions method is only supposed to reduce the possible range '
          'of movement, not increase it.\n'
          'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
          'viewport dimension is $viewportDimension.'
        );
      }
      return true;
    }());
    return result;
  }

physicsScrollPhysics的實(shí)例。
在進(jìn)入到physics.applyBoundaryConditions(this, value);applyBoundaryConditions方法中


 ///
  /// [BouncingScrollPhysics] returns zero. In other words, it allows scrolling
  /// past the boundary unhindered.
  ///
  /// [ClampingScrollPhysics] returns the amount by which the value is beyond
  /// the position or the boundary, whichever is furthest from the content. In
  /// other words, it disallows scrolling past the boundary, but allows
  /// scrolling back from being overscrolled, if for some reason the position
  /// ends up overscrolled.
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    if (parent == null)
      return 0.0;
    return parent.applyBoundaryConditions(position, value);
  }

注釋中,寫著BouncingScrollPhysics的滑動(dòng)不受阻礙,可以一直滑動(dòng)。也就是在iOS平臺(tái)的ScrollView中,可以一直下拉。也就是,我上面的demo效果。對(duì)于ClampingScrollPhysics無法繼續(xù)下拉。

  • step 5 parent的具體實(shí)現(xiàn)
    繼續(xù)debug源碼。

  • physics在Android中的實(shí)現(xiàn)

physics

physics.applyBoundaryConditions在Android中由 ClampingScrollPhysics 完成

ClampingScrollPhysics.applyBoundaryConditions


@override
  double applyBoundaryConditions(ScrollMetrics position, double value) {
    assert(() {
      if (value == position.pixels) {
        throw FlutterError(
          '$runtimeType.applyBoundaryConditions() was called redundantly.\n'
          'The proposed new position, $value, is exactly equal to the current position of the '
          'given ${position.runtimeType}, ${position.pixels}.\n'
          'The applyBoundaryConditions method should only be called when the value is '
          'going to actually change the pixels, otherwise it is redundant.\n'
          'The physics object in question was:\n'
          '  $this\n'
          'The position object in question was:\n'
          '  $position\n'
        );
      }
      return true;
    }());
    if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
      return value - position.pixels;
    if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
      return value - position.pixels;
    if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
      return value - position.minScrollExtent;
    if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
      return value - position.maxScrollExtent;
    return 0.0;
  }

  • physics在iOS中的具體實(shí)現(xiàn)
physics在iOS中的具體實(shí)現(xiàn)

physics.applyBoundaryConditions在iOS中由 BouncingScrollPhysics 完成

BouncingScrollPhysics.applyBoundaryConditions

  @override
  double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;

在Android平臺(tái)中,會(huì)對(duì)applyBoundaryConditions的返回值做處理,不為零的時(shí)候(看下step3),是會(huì)調(diào)用OverscrollNotification.onNotification;但是對(duì)于iOS平臺(tái),由于默認(rèn)一直返回0.0,故不會(huì)調(diào)用。

原來如此

由于我這里需要的是Android的效果,所以需要將physics的具體實(shí)現(xiàn)更改為ClampingScrollPhysics即可,正好,

ScrollView構(gòu)造

我們將physics的實(shí)現(xiàn)變更為ClampingScrollPhysics,完美解決。

屏幕快照 2019-02-24 下午11.51.38.png

拓展思維

如果,我們將physics的實(shí)現(xiàn)變更為BouncingScrollPhysics,會(huì)發(fā)生什么?

Feb-24-2019 23-55-02.gif

完美的在Android上實(shí)現(xiàn)了,同iOS一樣的可以一直下拉的listview效果。

彩蛋

思考為什么兩個(gè)平臺(tái)physics的具體實(shí)現(xiàn)不同

這個(gè)原因,也就是相當(dāng)于physics什么時(shí)候被初始化的。我就不娓娓道來了,我這邊翻閱并且debug源碼找到了出處。在scroll_configuration.dart文件中,有下面一段代碼:


  /// The scroll physics to use for the platform given by [getPlatform].
  ///
  /// Defaults to [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
  /// Android.
  ScrollPhysics getScrollPhysics(BuildContext context) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
        return const BouncingScrollPhysics();
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        return const ClampingScrollPhysics();
    }
    return null;
  }

可以看到,不同的平臺(tái),返回的值是不用的。返回的結(jié)果,也驗(yàn)證了我們剛才debug的結(jié)果。小驚喜:,看TargetPlatform.fuchsia,看來fuchsia系統(tǒng)即將到來。

Flutter要統(tǒng)一天下啊~

屏幕快照 2019-02-25 上午12.01.56.png

共勉

學(xué)習(xí)一門新系統(tǒng)知識(shí),一定要知其然并知其所以然。如果,我直接設(shè)置physics的值,不會(huì)學(xué)習(xí)到實(shí)質(zhì)性的知識(shí)。明白了原理才能掌控全局。之前看一些Android大神的博客,很多東西,都是翻閱源碼debug而來的。況且當(dāng)下Flutter的相關(guān)有深度有見地的資料不多的情況下,我也是被逼的,沒辦法。只有翻閱源碼了。翻過了源碼,卻獲得了意外之喜,收獲了更多知識(shí)。
最后一句話,與君共勉:勤而學(xué)之,柳暗花明又一村。

PS:最終實(shí)現(xiàn)的開頭效果的源碼與思路,里面涉及到手勢(shì)識(shí)別、類似Android的事件分發(fā)、動(dòng)畫、滑動(dòng)監(jiān)聽以及解刨源碼等等。估計(jì)要寫很多字~~有時(shí)間再來一篇博客。
flutter issues已經(jīng)提交了相關(guān)建議。

博客地址

Flutter 豆瓣客戶端,誠(chéng)心開源
Flutter Container
Flutter SafeArea
Flutter Row Column MainAxisAlignment Expanded
Flutter Image全解析
Flutter 常用按鈕總結(jié)
Flutter ListView豆瓣電影排行榜
Flutter Card
Flutter Navigator&Router(導(dǎo)航與路由)
OverscrollNotification不起效果引起的Flutter感悟分享
Flutter 上拉抽屜實(shí)現(xiàn)
Flutter 豆瓣客戶端,誠(chéng)心開源
Flutter 更改狀態(tài)欄顏色

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,654評(píng)論 4 61
  • 前兩天看到后海深圳灣片區(qū)的各種幕墻,集中的這個(gè)片區(qū)有不少的玻璃幕墻建筑。其中深圳灣一號(hào)的幕墻比其他建筑的要明亮,色...
    goldenwhale閱讀 165評(píng)論 0 0
  • 開店前期,你須把原材料備好分類號(hào)。 至少半個(gè)月到一個(gè)月的量。 用量開始的時(shí)候不可能算得很準(zhǔn)。 原則:寧多勿少! 注...
    芒果戰(zhàn)鱷魚閱讀 2,633評(píng)論 0 4
  • 我有一個(gè)朋友,十七歲師范畢業(yè)便從鷹潭跟著現(xiàn)在的丈夫來到我們這個(gè)小城安家立業(yè)。兩人一起分在城關(guān)小學(xué),每天雙...
    佛說隨喜閱讀 707評(píng)論 1 4
  • 今天很想寫寫我的爸爸,爸爸去世已經(jīng)一年多了,但他始終活在我的心里。每想起他,我都會(huì)想到曾經(jīng)所擁有的濃濃的父愛...
    治愈家庭教育閱讀 370評(píng)論 2 1

友情鏈接更多精彩內(nèi)容