Flutter 基於NestedScrollView+RefreshIndicator完美解決滑動衝突


NestedScrollView屬於Sliver系列控件,是對CustomScrollView的封裝。內部由多個controller控制器實現,與RefreshIndicator組合實現ListView下拉刷新時出現滑動BUG。

看完本篇文章,相信對於Flutter初學者有一定幫助。

DEMO地址:點擊下載

在這裏插入圖片描述

一、SliverAppbar 控件介紹

SliverAppBar “應用欄” 相當於升級版的 appbar 於 AppBar 位置的固定的應用最上面的; 而 SliverAppBar 是可以跟隨內容滾動的;

1、使用方法

  1. 與CustomScrollView、NestedScrollView集成的材質設計應用欄。應用欄由工具欄和其他小部件組成,例如 TabBar和FlexibleSpaceBar。應用欄通常會使用IconButton公開一個或多個常見操作,後者可選地後跟 PopupMenuButton以進行不太常見的操作
  2. 注意點:
    通常結合 CustomScrollView 、 NestedScrollView 來使用它, NestedScrollView裏面可以嵌套Listview 完成滑動

2、基本屬性

const SliverAppBar({
    Key key,
    this.leading,         //在標題左側顯示的一個控件,在首頁通常顯示應用的 logo;在其他界面通常顯示爲返回按鈕
    this.automaticallyImplyLeading = true,//? 控制是否應該嘗試暗示前導小部件爲null
    this.title,               //當前界面的標題文字
    this.actions,          //一個 Widget 列表,代表 Toolbar 中所顯示的菜單,對於常用的菜單,通常使用 IconButton 來表示;對於不常用的菜單通常使用 PopupMenuButton 來顯示爲三個點,點擊後彈出二級菜單
    this.flexibleSpace,        //一個顯示在 AppBar 下方的控件,高度和 AppBar 高度一樣, // 可以實現一些特殊的效果,該屬性通常在 SliverAppBar 中使用
    this.bottom,         //一個 AppBarBottomWidget 對象,通常是 TabBar。用來在 Toolbar 標題下面顯示一個 Tab 導航欄
    this.elevation,            //陰影
    this.forceElevated = false, 
    this.backgroundColor,       //APP bar 的顏色,默認值爲 ThemeData.primaryColor。改值通常和下面的三個屬性一起使用
    this.brightness,   //App bar 的亮度,有白色和黑色兩種主題,默認值爲 ThemeData.primaryColorBrightness
    this.iconTheme,  //App bar 上圖標的顏色、透明度、和尺寸信息。默認值爲 ThemeData().primaryIconTheme
    this.textTheme,    //App bar 上的文字主題。默認值爲 ThemeData().primaryTextTheme
    this.primary = true,  //此應用欄是否顯示在屏幕頂部
    this.centerTitle,     //標題是否居中顯示,默認值根據不同的操作系統,顯示方式不一樣,true居中 false居左
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,//橫軸上標題內容 周圍的間距
    this.expandedHeight,     //展開高度
    this.floating = false,       //是否隨着滑動隱藏標題
    this.pinned = false,  //是否固定在頂部
    this.snap = false,   //與floating結合使用
  })

3、常用屬性

  1. 在標題前面顯示的一個控件,在首頁通常顯示應用的logo;在其他界面通常顯示爲返回按鈕。
leading: Icon(_selectedChoice.icon) ,
  1. 控制是否應該嘗試暗示前導小部件爲null(如果有 leading 這個不會管用 ,相當於忽略這個參數 ; 如果沒有leading ,當有側邊欄的時候, false:不會顯示默認的圖片,true 會顯示 默認圖片,並響應打開側邊欄的事件)
automaticallyImplyLeading:true,
  1. 當前界面的標題文字
 title: Text(_selectedChoice.title )
  1. 一個 Widget 列表,代表 Toolbar 中所顯示的菜單,對於常用的菜單,通常使用 IconButton 來表示;對於不常用的菜單通常使用 PopupMenuButton 來顯示爲三個點,點擊後彈出二級菜單
actions: <Widget>[
     new IconButton( // action button
         icon: new Icon(choices[0].icon),
         onPressed: () { _select(choices[0]); },
     ),
     new IconButton( // action button
         icon: new Icon(choices[1].icon),
         onPressed: () { _select(choices[1]); },
     ),
     new PopupMenuButton<Choice>( // overflow menu
         onSelected: _select,
         itemBuilder: (BuildContext context) {
            return choices.skip(2).map((Choice choice) {
               return new PopupMenuItem<Choice>(
                  value: choice,
                  child: new Text(choice.title),
               );
            }).toList();
         },
      )
   ],
  1. 一個顯示在 AppBar 下方的控件,高度和 AppBar 高度一樣,可以實現一些特殊的效果,該屬性通常在 SliverAppBar 中使用
flexibleSpace: Container(
  color: Colors.blue,
  width: MediaQuery.of(context).size.width,
  child: Text("aaaaaaaaaa"),
    height: 10,
      )
  1. 一個 AppBarBottomWidget 對象,通常是 TabBar。用來在 Toolbar 標題下面顯示一個 Tab 導航欄
bottom: new TabBar(
            isScrollable: true,
            tabs: choices.map((Choice choice) {
              return new Tab(
                text: choice.title,
                icon: new Icon(choice.icon),
              );
            }).toList(),
          )
  1. 標題居中顯示
centerTitle: true,
  1. 此應用欄是否顯示在屏幕頂部
primary: true,
  1. appbar是否隨着滑動隱藏標題
 floating: true,
  1. tab 是否固定在頂部(爲true是固定,爲false是不固定)
pinned: true,
  1. 與floating結合使用,如果snap和floating爲true,則浮動應用欄將“捕捉”到視圖中
snap: true,
  1. 可滾動視圖的高度(默認高度是狀態欄和導航欄的高度,如果有滾動視差的話,要大於前兩者的高度)
expandedHeight: 200.0, 

二、NestedScrollView控件介紹

可以在其內部嵌套其他滾動視圖的滾動視圖,其滾動位置是固有鏈接的。

1、使用方法

  • 此窗口小部件最常見的用例是可滾動視圖,該視圖具有一個靈活的SliverAppBar(在標頭中包含TabBar)(由 headerSliv​​erBuilder構建,並且在主體中具有TabBarView),以便可滾動視圖的內容根據可見的選項卡而有所不同。
  • NestedScrollView通過爲外部ScrollView和內部ScrollView(位於TabBarView內部的ScrollController)提供自定義ScrollController,從而將它們鉤在一起,以便它們作爲一個連貫的滾動視圖顯示給用戶,從而解決了此問題 。

2、構造方法

const NestedScrollView({
    Key key,
    this.controller,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.physics,
    @required this.headerSliverBuilder,
    @required this.body,
    this.dragStartBehavior = DragStartBehavior.start,
  }) : assert(scrollDirection != null),
       assert(reverse != null),
       assert(headerSliverBuilder != null),
       assert(body != null),
       super(key: key);

3、與NestedScrollView集成SliverAppBar

@override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: choices.length,
        child: Scaffold(
            body: NestedScrollView(
              headerSliverBuilder: _headerSliverBuilder,
              body : TabBarView(
                children: choices.map((Choice choice) {
                  return new Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: new ChoiceCard(choice: choice),
                  );
                }).toList(),
              ),
            )
        )
    );
  }

三、RefreshIndicator與NestedScrollView滑動列表衝突同步解決

RefreshIndicator與NestedScrollView組合下拉刷新是開發過程中常見的一種效果,但是由於NestedScrollView源碼中有有內外兩個controller控制器。(out控制header,inner控制body。只有當out不能滾動了纔會滾動inner)

與RefreshIndicator組合使用時,下拉刷新效果失效。

  • 首先,我調試到這個,發現notification.depth不爲0,其實也好理解,因爲NestedScrollView裏面有很多能滾動的東西。默認的RefreshIndicator要求的是必須是第一層的它才其效果。
bool defaultScrollNotificationPredicate(ScrollNotification notification) {
  return notification.depth == 0;
}
  • 其次,我調試到_handleScrollNotification方法中,我們可以看到會有很多ScrollNotification進來,不同的,當你滑動在一個不能滾動的list裏面的時候,獲取的viewportDimension是爲0.。這會覆蓋掉之前有viewportDimension的值。
  //定義變量接收臨時值
  double tempHeight = 0;

  bool _handleScrollNotification(ScrollNotification notification) {
    if (!widget.notificationPredicate(notification)) return false;
    if (notification is ScrollStartNotification &&
        notification.metrics.extentBefore == 0.0 &&
        _mode == null &&
        _start(notification.metrics.axisDirection)) {
      setState(() {
        _mode = _RefreshIndicatorMode.drag;
      });
      return false;
    }

    bool indicatorAtTopNow;

    switch (notification.metrics.axisDirection) {
      case AxisDirection.down:
        indicatorAtTopNow = true;
        break;
      case AxisDirection.up:
        indicatorAtTopNow = false;
        break;
      case AxisDirection.left:
      case AxisDirection.right:
        indicatorAtTopNow = null;
        break;
    }
	// 當notification.metrics.viewportDimension時,
	// tempHeight 賦值notification.metrics.viewportDimension
    if (notification.metrics.viewportDimension > 0) {
      tempHeight = notification.metrics.viewportDimension;
    }

    if (indicatorAtTopNow != _isIndicatorAtTop) {
      if (_mode == _RefreshIndicatorMode.drag ||
          _mode == _RefreshIndicatorMode.armed)
        _dismiss(_RefreshIndicatorMode.canceled);
    } else if (notification is ScrollUpdateNotification) {
      if (_mode == _RefreshIndicatorMode.drag ||
          _mode == _RefreshIndicatorMode.armed) {
        if (notification.metrics.extentBefore > 0.0) {
          _dismiss(_RefreshIndicatorMode.canceled);
        } else {
          _dragOffset -= notification.scrollDelta;

          print(tempHeight);
          _checkDragOffset(tempHeight);
        }
      }
      if (_mode == _RefreshIndicatorMode.armed &&
          notification.dragDetails == null) {
        // On iOS start the refresh when the Scrollable bounces back from the
        // overscroll (ScrollNotification indicating this don't have dragDetails
        // because the scroll activity is not directly triggered by a drag).
        _show();
      }
    } else if (notification is OverscrollNotification) {
      if (_mode == _RefreshIndicatorMode.drag ||
          _mode == _RefreshIndicatorMode.armed) {
        _dragOffset -= notification.overscroll / 2.0;
        _checkDragOffset(tempHeight);
      }
    } else if (notification is ScrollEndNotification) {
      switch (_mode) {
        case _RefreshIndicatorMode.armed:
          _show();
          break;
        case _RefreshIndicatorMode.drag:
          _dismiss(_RefreshIndicatorMode.canceled);
          break;
        default:
          // do nothing
          break;
      }
    }
    return false;
  }
  • 最後在使用RefreshIndicator時,添加notificationPredicate屬性
RefreshIndicator(
     notificationPredicate: (notifation) {
     // 返回true即可
       return true;
     },
     onRefresh: () {
       return Future.delayed(Duration(seconds: 2), () {
         return true;
       });
     },
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章