Flutter第十章(Scrollable ,Scrollbar 、CupertinoScrollbar ,SliverList、SliverFixedExtentList、 SliverG...

版權聲明:本文為作者原創書籍。轉載請注明作者和出處,未經授權,嚴禁私自轉載,侵權必究!!!

情感語錄: 如果你是對的,你沒必要發脾氣;如果你是錯的,你沒資格去發脾氣。這才是真正的智慧。

歡迎來到本章節,上一章節我們講了常用表單的使用,知識點回顧 戳這里 Flutter基礎第九章

本章節主要講解可以滾動的組件,很多時候一個頁面內容會相當多;當組件內容超過當前顯示視圖范圍時,如果沒有特殊處理,Flutter 則會提示Overflow錯誤;在第二章中我們有講到 ListViewGridView 可以實現滾動效果,利用這兩個組件可以規避這類問題。但是有些復雜的界面交互效果,使用它們可能就變得非常不友好了。假如有一個頁面,頂部需要一個GridView,底部需要一個ListView,而要求整個頁面的滑動效果是統一的,即它們看起來是一個整體。此時如果使用GridView+ListView來實現的話,就不能保證一致的滑動效果,因為它們的滾動效果是分離的,所以這時就需要一個"膠水",把這些彼此獨立的可滾動組件"粘"起來。

本章簡要:

1、Scrollable 組件

2、Scrollbar 、CupertinoScrollbar 組件

3、SingleChildScrollView、 CustomScrollView 組件

4、SliverList、SliverFixedExtentList、 SliverGrid 組件

5、SliverPadding 、SliverAppBar 組件

6、ScrollController 滾動監聽和控制

一、Scrollable 組件

可滾動組件都直接或間接包含一個Scrollable 組件,比如我們之前學的ListViewGridView 組件,而他們的父類 BoxScrollView 是繼承與ScrollView實現的,而 ScrollView中就包含了 Scrollable 組件。 所以可滾動類組件包括一些共同的屬性:

Scrollable構造函數:

    Scrollable({
      ...// 去除部分屬性
      this.axisDirection = AxisDirection.down,
      this.controller,
      this.physics,
      @required this.viewportBuilder, //進階篇講
    })

axisDirection : 滾動方向。

physics :此屬性接受一個ScrollPhysics類型的對象,Flutter會根據具體平臺分別使用不同的ScrollPhysics對象,應用不同的顯示效果:

  ClampingScrollPhysics:Android下微光效果。

  BouncingScrollPhysics:iOS下彈性效果。

controller:此屬性接受一個ScrollController對象。ScrollController的主要作用是控制滾動位置和監聽滾動事件。

二、Scrollbar 、 CupertinoScrollbar 組件

Scrollbar是一個Material 風格的滾動條,如果要給可滾動組件添加滾動條,只需將 Scrollbar作為可滾動組件的任意一個父級組件即可,如:

    Scrollbar(
      child: SingleChildScrollView(
        ...
      ),
    );

CupertinoScrollbar 是iOS風格的滾動條,如果你使用的是Scrollbar,那么在iOS平臺它會自動切換為CupertinoScrollbar。

三、SingleChildScrollView、 CustomScrollView 組件

1、SingleChildScrollView 組件

SingleChildScrollView類似于Android中的 ScrollView,它只能接收一個子組件。它是繼承 StatelessWidget 實現的,并非 繼承 ScrollView。需要注意的是,通常SingleChildScrollView 只應在期望的內容不會超過屏幕太多時使用,因為SingleChildScrollView不支持基于Sliver的延遲實例化模型,所以如果預計視圖可能包含超出屏幕尺寸太多的內容時,那么使用SingleChildScrollView將會 非常耗性能,此時應該使用一些支持Sliver延遲加載的可滾動組件,如: ListView。

構造函數:

  const SingleChildScrollView({
    Key key,
    this.scrollDirection = Axis.vertical,
    this.reverse = false,
    this.padding,
    bool primary,
    this.physics,
    this.controller,
    this.child,
    this.dragStartBehavior = DragStartBehavior.start,
  })

除了上面的通用屬性外,它下面的兩個屬性也非常有用:

reverse:該屬性表示是否反向,就是說初始滾動位置是在“頭”還是“尾”,取false時,初始滾動位置在“頭”,反之則在“尾”,默認為false。

primary:指是否使用widget樹中默認的 PrimaryScrollController;當滑動方向為垂直方向(scrollDirection值為Axis.vertical)并且沒有指定controller時,primary默認為true.

簡單運用:

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
              appBar: AppBar(
                title: Text("ScrollerViewPage"),
              ),
              body: Container(
                  child: SingleChildScrollView(
                      reverse: false,
                      child: SingleChildScrollViewDemo()))));
    }
  }

  class SingleChildScrollViewDemo extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      List<Widget> listWidget = new List();

      for (int i = 0; i < 40; i++) {
        var text = Text("Item$i",
            style: TextStyle(fontSize: 16, color: Colors.redAccent));
        listWidget.add(text);
        listWidget.add(Divider());
      }
      return Column(children: listWidget);
    }
  }

效果如下:

SingleChildScrollView.gif

從效果可以看出 SingleChildScrollView 實現了 ListView 的滾動效果,但是需要注意的時 SingleChildScrollView 包裹的內容不能太多,否則引起性能問題。

2、CustomScrollView 組件

默認場景下,Scalfold 的導航欄都是固定寫死的,如果要做一些交互性或者是沉浸式的交互比較困難。Flutter 提供了 CustomScrollView 來幫助實現跟隨列表滑動發生一些變化的 AppBar 效果。CustomScrollView 繼承自 ScrollView,它可以完成 SingleChildScrollView 能完成的工作,且 CustomScrollView 是可滾動模型。

構造函數:

  const CustomScrollView({
    Key key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap = false,
    Key center,
    double anchor = 0.0,
    double cacheExtent,
    this.slivers = const <Widget>[],
    int semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
  })

除了上面講過的幾個屬性外,下面來看幾個其他重要的屬性。

shrinkWrap 配置可控制 AppBar 下的內容在滾動時,是否可以超過 AppBar 的邊界。如果為true,則滑動內容區域 可覆蓋 AppBar 直到屏幕頂端。如果 為 false ,則只能在 AppBar 以下區域滑動,即 AppBar 始終在頂部顯示。

anchor 該屬性不太好描述,它的的取值范圍值0.0-1.0之間,這個屬性迫使組件將其自身定位在父組件中的某個相對或絕對位置。后面演示觀察下。

slivers List<Widget> 類型,用來承載滑動內容

四、 SliverList、SliverFixedExtentList、 SliverGrid 組件

1、 這三個組件你可以理解是 Sliver版 (ListView(SliverList、SliverFixedExtentList)、GridView(SliverGrid))組件。SliverList 就是一個 ListView,只不過在整個實現上,需要指定一個 delegate,比如通過 SliverChildBuilderDelegate 進行列表的構建。

   SliverList(
      delegate: new SliverChildBuilderDelegate(
              (BuildContext context, int index) {
            //創建列表項
            return new Container(
              alignment: Alignment.center,
              child: new Text('list item $index'),
            );
          },
          childCount: 50 //50個列表項
      ),
    ),

SliverFixedExtentList 你可以看做是 SliverList的加強版,它可以使用 itemExtent 屬性來控制 item 的范圍(高度)。

2、SliverGrid 如同 GridView 一樣,只不過在整個實現上,需要指定一個 delegate 和 gridDelegate,比如通過 SliverChildBuilderDelegate 進行列表的構建,SliverGridDelegateWithFixedCrossAxisCount 來控制列數和行間距和列間距

  SliverPadding(
          padding:  EdgeInsets.all(8.0),
          sliver:  SliverGrid( //Grid
            gridDelegate:  SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2, //Grid按兩列顯示
              mainAxisSpacing: 10.0,
              crossAxisSpacing: 10.0,
              childAspectRatio: 4.0,
            ),
            delegate: new SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                //創建子widget
                return new Container(
                  alignment: Alignment.center,
                  color: Colors.cyan[100 * (index % 9)],
                  child: new Text('grid item $index'),
                );
              },
              childCount: 20,
            ),
          ),
        ),

五、SliverPadding 、SliverAppBar 組件

1、SliverPadding 組件同 前面講的 Padding 組件是一樣的,只不過 SliverPadding 接收的子組件是 sliver Widget。

  const SliverPadding({
    Key key,
    @required this.padding,
    Widget sliver,
  })

2、SliverAppBar 組件 是專門服務于 CustomScrollView 的; SliverAppBar 支持的屬性和 AppBar 支持的屬性基本無異,SliverAppBar 因為需要跟隨 ScrollView 的一些操作,屬性會多一些。

構造函數:

  const SliverAppBar({
    Key key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation,
    this.forceElevated = false,
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.actionsIconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    this.expandedHeight,
    this.floating = false,
    this.pinned = false,
    this.snap = false,
    this.shape,
  }) 

除開和AppBar 中屬性外,下面來看幾個重要屬性:

floating: false 表示當列表往下滑動時,會先將列表內容滾動到頂部,然后再將 SliverAppBar 浮動出現,true表示當列表往下滑動時,會先將 SliverAppBar 浮動出現(與列表是否滾動到頂部無關),然后再繼續列表的滑動。

pinned 屬性能夠決定是否將導航欄部分固定。true 表示導航欄不會完全消失,否則隨滾動逐漸消失。

expandedHeight 屬性可以配置AppBar 展開后的高度。

flexibleSpace 屬性可以定制 AppBar 展開后的樣式,結合 FlexibleSpaceBar 控制。

下面來綜合運用下,這幾個新組建:

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {

      return CustomScrollViewDemo();
    }
  }

  class CustomScrollViewDemo extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Material(
        color: Colors.green,
        child: CustomScrollView(
          //距離 AppBar 百分之10 高度
          anchor: 0.1,
          slivers: <Widget>[
            //AppBar,包含一個導航欄
            SliverAppBar(
              pinned: true,
              expandedHeight: 200.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('ScrollerViewPage'),
               //centerTitle: true,
                background: Image.asset(
                  "images/mm.jpg",
                  fit: BoxFit.cover,
                ),
              ),
            ),

            SliverPadding(
              padding: EdgeInsets.all(8.0),
              sliver: SliverGrid(
                //Grid
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2, //Grid按兩列顯示
                  mainAxisSpacing: 10.0,
                  crossAxisSpacing: 10.0,
                  childAspectRatio: 4.0,
                ),
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    //創建子widget
                    return Container(
                      alignment: Alignment.center,
                      color: Colors.red[100 * ((index + 2) % 9)],
                      child: Text('grid item $index'),
                    );
                  },
                  childCount: 20,
                ),
              ),
            ),
            //List
            SliverFixedExtentList(
              itemExtent: 50.0,
              delegate: new SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                //創建列表項
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * ((index + 2) % 9)],
                  child: Text('list item $index'),
                );
              }, childCount: 50 //50個列表項
                  ),
            ),
          ],
        ),
      );
    }
  }

效果如下:

綜合運用.gif

可以看出 在設置 anchor 屬性后 底部的 SliverGrid 和 SliverAppBar 之間產生了整個父容器的百分之10距離,且上滑動時 SliverGrid 滾動超過這段距離后才會把 SliverAppBar 往上頂。下滑時 SliverAppBar 完全展開后才逐漸顯示這段距離。

六、ScrollController 滾動監聽和控制

本章和前面章節講解的 ListView 、 GridView;都是可滾動組件,在開發中很多時候需要監聽 這些可滾動組件的滾動位置信息,比如,滾動到某一個距離后要顯示一個視圖,再或者需要從某一位置需要跳到另一個位置時等。在 Flutter 中 ScrollController 能幫助我們解決這類問題。

構造函數:

  ScrollController({
    double initialScrollOffset = 0.0,
    this.keepScrollOffset = true,
    this.debugLabel,
  }) 

ScrollController 中常用的屬性和方法:

initialScrollOffset : 初始滾動位置。

keepScrollOffset : 是否保存滾動位置。

addListener() : 添加滾動位置信息監聽。

removeListener() : 刪除滾動位置信息監聽。

offset: 返回滑動距離的像素單位值。

dispose(): 銷毀 ScrollController 控制器。

jumpTo(): 跳轉到某一位置,不帶動畫。

animateTo(): 跳轉到某一位置,帶動畫效果,Curves 給我們提供了 幾十種動畫效果可供選擇。

下面來將上面的例子進行改造下,我需要監聽 CustomScrollView 的滑動信息,當滑動距離超過500個像素后 我要在屏幕上顯示一個回到頂部的按鈕,然后點擊按鈕直接回到頂部去。這里面設計到界面模板的刷新 需要繼承 StatefulWidget 來改造,且需要結合前面學的 FloatingActionButton 組件去實現(如果忘了,請回顧前面知識點)

  import 'package:flutter/material.dart';

  class ScrollerViewPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {

      return CustomScrollViewDemo();
    }
  }



  class CustomScrollViewDemo extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
      return Material(
          child: ScrollListenerDemo()
      );
    }
  }

  class ScrollListenerDemo extends StatefulWidget {
    @override
    _ScrollListenerDemoState createState() => _ScrollListenerDemoState();
  }

  class _ScrollListenerDemoState extends State<ScrollListenerDemo> {

    ScrollController _controller = new ScrollController();
    //是否顯示“返回到頂部”按鈕
    bool showToTopBtn = true;

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

    @override
    void dispose() {
      //為了避免內存泄露,需要調用_controller.dispose
      _controller.dispose();
      super.dispose();
    }

    @override
    Widget build(BuildContext context) {
      return Scaffold(

          floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
            backgroundColor: Colors.deepPurple,
              child: Icon(Icons.arrow_upward),
              onPressed: () {
                //返回到頂部時執行動畫
                _controller.animateTo(0.0,
                    duration: Duration(milliseconds: 1000),
                    curve: Curves.easeInBack
                );
              }
          ),

        body: CustomScrollView(
          controller: _controller,
          slivers: <Widget>[
            //AppBar,包含一個導航欄
            SliverAppBar(
              pinned: true,
              expandedHeight: 200.0,
              flexibleSpace: FlexibleSpaceBar(
                title: Text('ScrollerViewPage'),
                //centerTitle: true,
                background: Image.asset(
                  "images/mm.jpg",
                  fit: BoxFit.cover,
                ),
              ),
            ),

            SliverPadding(
              padding: EdgeInsets.all(8.0),
              sliver: SliverGrid(
                //Grid
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2, //Grid按兩列顯示
                  mainAxisSpacing: 10.0,
                  crossAxisSpacing: 10.0,
                  childAspectRatio: 4.0,
                ),
                delegate: SliverChildBuilderDelegate(
                      (BuildContext context, int index) {
                    //創建子widget
                    return Container(
                      alignment: Alignment.center,
                      color: Colors.red[100 * ((index + 2) % 9)],
                      child: Text('grid item $index'),
                    );
                  },
                  childCount: 20,
                ),
              ),
            ),
            //List
            SliverFixedExtentList(
              itemExtent: 50.0,
              delegate: new SliverChildBuilderDelegate(
                      (BuildContext context, int index) {
                    //創建列表項
                    return Container(
                      alignment: Alignment.center,
                      color: Colors.lightBlue[100 * ((index + 2) % 9)],
                      child: Text('list item $index'),
                    );
                  }, childCount: 50 //50個列表項
              ),
            ),
          ],
        )
      );
    }
  }

效果如下:

ScrollController監聽控制.gif

本章節的東西不多,但是值得去研究的內容還有很多,更多高級用法還需自己去探索。

好了本章節到此結束,又到了說再見的時候了,如果你喜歡請留下你的小紅星,你們的支持才是創作的動力,如有錯誤,請熱心的你留言指正, 謝謝大家觀看,下章再會 O(∩_∩)O

實例源碼地址: https://github.com/zhengzaihong/flutter_learn

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容