Flutter了解之入門(mén)篇6(可滾動(dòng)組件)

目錄
  1. ListView
  2. SingleChildScrollView
  3. GridView(二維網(wǎng)格列表)

Flutter官方并沒(méi)有對(duì)Widget進(jìn)行分類(lèi),對(duì)其分類(lèi)主要是為了對(duì)Widget進(jìn)行功能區(qū)分。

當(dāng)組件超過(guò)顯示窗口時(shí),F(xiàn)lutter會(huì)提示Overflow錯(cuò)誤。為此,F(xiàn)lutter提供了多種可滾動(dòng)組件用于顯示列表和長(zhǎng)布局。

/*
Flutter有兩種布局模型:
    1. 基于RenderBox的盒模型布局。
    2. 基于RenderSliver (Sliver) 按需加載列表布局。
*/
主縱軸
  滾動(dòng)方向稱(chēng)為主軸,非滾動(dòng)方向稱(chēng)為縱軸。

可滾動(dòng)組件的組成部分
  1. Scrollable (繼承自StatefulWidget) 
    處理滑動(dòng)手勢(shì),確定滑動(dòng)偏移,滑動(dòng)偏移變化時(shí)構(gòu)建Viewport 。
  2. Viewport
    渲染當(dāng)前視口中需要顯示的Sliver。
    父組件為Scrollable。
  3. Sliver
    對(duì)子組件進(jìn)行構(gòu)建和布局。
    父組件為Viewport。

具體過(guò)程
  1. Scrollable監(jiān)聽(tīng)到用戶(hù)滑動(dòng)行為后,根據(jù)最新的滑動(dòng)偏移構(gòu)建Viewport 。
  2. Viewport將當(dāng)前視口信息和配置信息通過(guò)SliverConstraints傳遞給Sliver。
  3. Sliver中對(duì)子組件(RenderBox)按需進(jìn)行構(gòu)建和布局,然后確認(rèn)自身的位置、繪制等信息,保存在geometry(SliverGeometry類(lèi)型的對(duì)象)中。

基于Sliver的延遲構(gòu)建模型
  通常可滾動(dòng)組件的子組件非常多、占用高度非常大,如果一次性將子組件全部構(gòu)建將會(huì)非常昂貴。為此,F(xiàn)lutter提出Sliver(薄片)概念,如果一個(gè)可滾動(dòng)組件支持Sliver模型,則可以分成許多Sliver,且只有當(dāng)Sliver出現(xiàn)在視口中時(shí)才去構(gòu)建它。
  支持:ListView、GridView。不支持:SingleChildScrollView。

公共屬性(最終會(huì)透?jìng)鹘oScrollable和Viewport)
  1. scrollDirection
    滾動(dòng)方向。
    Axis.vertica垂直方向(默認(rèn)),Axis.horizontal水平方向。
  2. reverse
    是否按照閱讀方向相反的方向滑動(dòng)。
    決定可滾動(dòng)組件的初始滾動(dòng)位置是在“頭”還是“尾”,取false時(shí)初始滾動(dòng)位置在“頭”,反之則在“尾”。
  3. primary
    是否使用widget樹(shù)中默認(rèn)的PrimaryScrollController;
    當(dāng)滑動(dòng)方向?yàn)榇怪狈较蚯覜](méi)有指定controller時(shí),primary默認(rèn)為true。
  4. padding
    內(nèi)邊距
  5. controller(ScrollController類(lèi)型)
    控制滾動(dòng)位置和監(jiān)聽(tīng)滾動(dòng)事件。
    當(dāng)子樹(shù)中的可滾動(dòng)組件沒(méi)有顯式指定controller且primary屬性值為true時(shí)(默認(rèn)就為true),可滾動(dòng)組件會(huì)使用Widget樹(shù)中默認(rèn)的PrimaryScrollController。這種機(jī)制的好處是父組件可以控制子樹(shù)中可滾動(dòng)組件的滾動(dòng)行為,例如,Scaffold正是使用這種機(jī)制在iOS中實(shí)現(xiàn)了點(diǎn)擊導(dǎo)航欄回到頂部的功能。
  6. physics(ScrollPhysics類(lèi)型)
    決定可滾動(dòng)組件如何響應(yīng)用戶(hù)操作。比如用戶(hù)滑動(dòng)完抬起手指后,繼續(xù)執(zhí)行動(dòng)畫(huà);或者滑動(dòng)到邊界時(shí),如何顯示。NeverScrollableScrollPhysics():禁止?jié)L動(dòng)。
    Flutter默認(rèn)會(huì)根據(jù)各平臺(tái)分別使用不同的ScrollPhysics對(duì)象,應(yīng)用不同的顯示效果,如當(dāng)滑動(dòng)到邊界時(shí),繼續(xù)拖動(dòng)的話,在iOS上會(huì)出現(xiàn)彈性效果,而在Android上會(huì)出現(xiàn)微光效果。如果想在所有平臺(tái)下使用同一種效果,可以顯式指定:
      1. ClampingScrollPhysics:Android下微光效果。
      2. BouncingScrollPhysics:iOS下彈性效果。


cacheExtent
    預(yù)渲染的高度(下圖中頂部和底部灰色的區(qū)域)。
    如果RenderBox進(jìn)入這個(gè)區(qū)域,即使它未顯示在屏幕上,也要先進(jìn)行構(gòu)建,預(yù)渲染是為了后面進(jìn)入Viewport時(shí)更流暢。
    默認(rèn)值是250,在構(gòu)建可滾動(dòng)列表時(shí)可以指定這個(gè)值(最終會(huì)傳給 Viewport)。
ListView
  1. Scrollable、Viewport、Sliver
Scrollable({
  this.axisDirection = AxisDirection.down,  // 滾動(dòng)方向
  this.controller,
  this.physics,
  // 滑動(dòng)時(shí)Scrollable會(huì)調(diào)用此回調(diào)構(gòu)建新的Viewport,同時(shí)傳遞一個(gè)ViewportOffset類(lèi)型的offset參數(shù)(描述Viewport該顯示哪一部分)。
  // 重新構(gòu)建Viewport(本身也是Widget,只是配置信息)不是一個(gè)昂貴的操作,Viewport變化時(shí)對(duì)應(yīng)的RenderViewport會(huì)更新信息,并不會(huì)隨著Widget進(jìn)行重新構(gòu)建。
  @required this.viewportBuilder, 
})
Viewport({
  Key? key,
  this.axisDirection = AxisDirection.down,
  this.crossAxisDirection,
  this.anchor = 0.0,
  // 滾動(dòng)偏移。Scrollabel構(gòu)建Viewport 時(shí)傳入(描述了Viewport該顯示哪一部分)。
  required ViewportOffset offset, 
  // 類(lèi)型為Key,表示從什么地方開(kāi)始繪制,默認(rèn)是第一個(gè)元素
  this.center,
  this.cacheExtent, // 預(yù)渲染區(qū)域
  // pixel:cacheExtent值為預(yù)渲染區(qū)域的具體像素長(zhǎng)度
  // viewport:cacheExtent值是一個(gè)乘數(shù),預(yù)渲染區(qū)域的像素長(zhǎng)度=cacheExtent*viewport。
  this.cacheExtentStyle = CacheExtentStyle.pixel, 
  this.clipBehavior = Clip.hardEdge,
  List<Widget> slivers = const <Widget>[], // 需要顯示的 Sliver 列表
})
Sliver對(duì)應(yīng)的渲染對(duì)象類(lèi)型是RenderSliver。
RenderSliver和RenderBox的相同點(diǎn)是都繼承自RenderObject類(lèi),不同點(diǎn)是在布局時(shí)約束信息不同。RenderBox在布局時(shí)父組件傳遞給它的約束信息是BoxConstraints(最大最小寬高約束);而 RenderSliver在布局時(shí)父組件傳遞給它的約束是SliverConstraints。
  1. Scrollbar (Material風(fēng)格的滾動(dòng)條)

使用:作為可滾動(dòng)組件的任意一個(gè)父組件即可。

Scrollbar(
  child: SingleChildScrollView(
    ...
  ),
);
Scrollbar在iOS平臺(tái)會(huì)自動(dòng)切換為CupertinoScrollbar(iOS風(fēng)格)。
Scrollbar和CupertinoScrollbar都是通過(guò)監(jiān)聽(tīng)滾動(dòng)通知來(lái)確定滾動(dòng)條位置的。
  1. ScrollController(間接繼承自Listenable)

可滾動(dòng)組件都有一個(gè)controller屬性(控制和監(jiān)聽(tīng)滾動(dòng))

ScrollController({
  double initialScrollOffset = 0.0, // 初始滾動(dòng)位置
  this.keepScrollOffset = true,// 是否保存滾動(dòng)位置
  ...
})
監(jiān)聽(tīng)滾動(dòng)事件
  controller.addListener(()=>print(controller.offset))

常用的屬性和方法:
1. offset
  可滾動(dòng)組件當(dāng)前的滾動(dòng)位置。
2. jumpTo(double offset)、animateTo(double offset,...)
  用于跳轉(zhuǎn)到指定的位置,不同之處在于,后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫(huà)。

示例

創(chuàng)建一個(gè)ListView,判斷當(dāng)前位置是否超過(guò)1000像素,如果超過(guò)則在屏幕右下角顯示一個(gè)“返回頂部”的按鈕,該按鈕點(diǎn)擊后可以使ListView恢復(fù)到初始位置;如果沒(méi)有超過(guò)1000像素,則隱藏“返回頂部”按鈕。

class ScrollControllerTestRoute extends StatefulWidget {
  @override
  ScrollControllerTestRouteState createState() {
    return new ScrollControllerTestRouteState();
  }
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
  ScrollController _controller = new ScrollController();
  bool showToTopBtn = false; // 是否顯示“返回到頂部”按鈕
  @override
  void initState() {
    super.initState();
    // 監(jiān)聽(tīng)滾動(dòng)事件,打印滾動(dòng)位置
    _controller.addListener(() {
      print(_controller.offset); //打印滾動(dòng)位置
      if (_controller.offset < 1000 && showToTopBtn) {
        setState(() {
          showToTopBtn = false;
        });
      } else if (_controller.offset >= 1000 && showToTopBtn == false) {
        setState(() {
          showToTopBtn = true;
        });
      }
    });
  }
  @override
  void dispose() {
    // 為了避免內(nèi)存泄露,需要調(diào)用_controller.dispose
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("滾動(dòng)控制")),
      body: Scrollbar(
        child: ListView.builder(
            itemCount: 100,
            itemExtent: 50.0, // 列表項(xiàng)高度固定時(shí),顯式指定高度是一個(gè)好習(xí)慣(性能消耗小)
            controller: _controller,
            itemBuilder: (context, index) {
              return ListTile(title: Text("$index"),);
            }
        ),
      ),
      floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
          child: Icon(Icons.arrow_upward),
          onPressed: () {
            //返回到頂部時(shí)執(zhí)行動(dòng)畫(huà)
            _controller.animateTo(.0,
                duration: Duration(milliseconds: 200),
                curve: Curves.ease
            );
          }
      ),
    );
  }
}


滾動(dòng)位置恢復(fù)

PageStorage是一個(gè)用于保存頁(yè)面(路由)相關(guān)數(shù)據(jù)的功能型組件,它擁有一個(gè)存儲(chǔ)桶,子樹(shù)中的Widget可以通過(guò)指定不同的PageStorageKey來(lái)存儲(chǔ)各自的數(shù)據(jù)或狀態(tài)。

每次滾動(dòng)結(jié)束,可滾動(dòng)組件都會(huì)將滾動(dòng)位置offset存儲(chǔ)到PageStorage中,當(dāng)可滾動(dòng)組件重新創(chuàng)建時(shí)再恢復(fù)。
  ScrollController.keepScrollOffset為false,則滾動(dòng)位置將不會(huì)被存儲(chǔ),可滾動(dòng)組件重新創(chuàng)建時(shí)會(huì)使用ScrollController.initialScrollOffset;
  ScrollController.keepScrollOffset為true時(shí),可滾動(dòng)組件在第一次創(chuàng)建時(shí),會(huì)滾動(dòng)到initialScrollOffset處,因?yàn)檫@時(shí)還沒(méi)有存儲(chǔ)過(guò)滾動(dòng)位置。在接下來(lái)的滾動(dòng)中就會(huì)存儲(chǔ)、恢復(fù)滾動(dòng)位置,忽略initialScrollOffset。

當(dāng)一個(gè)路由中包含多個(gè)可滾動(dòng)組件時(shí),如果發(fā)現(xiàn)在進(jìn)行一些跳轉(zhuǎn)或切換操作后,滾動(dòng)位置不能正確恢復(fù),這時(shí)可以通過(guò)顯式指定不同的PageStorageKey來(lái)分別跟蹤不同的可滾動(dòng)組件的位置,如:
  ListView(key: PageStorageKey(1), ... );
  ListView(key: PageStorageKey(2), ... );

注意:一個(gè)路由中包含多個(gè)可滾動(dòng)組件時(shí),如果要分別跟蹤它們的滾動(dòng)位置,并非一定就得給他們分別提供PageStorageKey。這是因?yàn)镾crollable本身是一個(gè)StatefulWidget,它的狀態(tài)中也會(huì)保存當(dāng)前滾動(dòng)位置,所以,只要可滾動(dòng)組件本身沒(méi)有被從樹(shù)上detach掉,那么其State就不會(huì)銷(xiāo)毀,滾動(dòng)位置就不會(huì)丟失。只有當(dāng)Widget發(fā)生結(jié)構(gòu)變化,導(dǎo)致可滾動(dòng)組件的State銷(xiāo)毀或重新構(gòu)建時(shí)才會(huì)丟失狀態(tài),這種情況就需要顯式指定PageStorageKey,通過(guò)PageStorage來(lái)存儲(chǔ)滾動(dòng)位置,一個(gè)典型的場(chǎng)景是在使用TabBarView時(shí),在Tab發(fā)生切換時(shí),Tab頁(yè)中的可滾動(dòng)組件的State就會(huì)銷(xiāo)毀,這時(shí)如果想恢復(fù)滾動(dòng)位置就需要指定PageStorageKey。

ScrollPosition

真正保存滑動(dòng)位置信息的對(duì)象。
  offset只是一個(gè)便捷屬性:double get offset => position.pixels;

一個(gè)ScrollController對(duì)象可以同時(shí)被多個(gè)可滾動(dòng)組件使用,ScrollController會(huì)為每一個(gè)可滾動(dòng)組件創(chuàng)建一個(gè)ScrollPosition對(duì)象,并保存在positions屬性中(List<ScrollPosition>)。
  controller.positions.elementAt(0).pixels
  controller.positions.elementAt(1).pixels
  controller.positions.length 被幾個(gè)可滾動(dòng)組件使用

ScrollController的animateTo() 和 jumpTo(),內(nèi)部最終都會(huì)調(diào)用ScrollPosition的同名方法(真正來(lái)控制跳轉(zhuǎn)滾動(dòng)位置)。

ScrollController控制原理

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

當(dāng)ScrollController和可滾動(dòng)組件關(guān)聯(lián)時(shí),可滾動(dòng)組件首先會(huì)調(diào)用ScrollController的createScrollPosition()方法來(lái)創(chuàng)建一個(gè)ScrollPosition來(lái)存儲(chǔ)滾動(dòng)位置信息,接著,可滾動(dòng)組件會(huì)調(diào)用attach()方法,將創(chuàng)建的ScrollPosition添加到ScrollController的positions屬性中,這一步稱(chēng)為“注冊(cè)位置”,只有注冊(cè)后animateTo() 和 jumpTo()才可以被調(diào)用。

當(dāng)可滾動(dòng)組件銷(xiāo)毀時(shí),會(huì)調(diào)用ScrollController的detach()方法,將其ScrollPosition對(duì)象從ScrollController的positions屬性中移除,這一步稱(chēng)為“注銷(xiāo)位置”,注銷(xiāo)后animateTo() 和 jumpTo() 將不能再被調(diào)用。

注意:ScrollController的animateTo() 和 jumpTo()內(nèi)部會(huì)調(diào)用【所有】ScrollPosition的同名方法。

滾動(dòng)監(jiān)聽(tīng)

Flutter Widget樹(shù)中子Widget可以通過(guò)發(fā)送通知(Notification)與父(包括祖先)Widget通信。父級(jí)組件可以通過(guò)NotificationListener組件來(lái)監(jiān)聽(tīng)自己關(guān)注的通知,這種通信方式類(lèi)似于Web開(kāi)發(fā)中瀏覽器的事件冒泡。

可滾動(dòng)組件在滾動(dòng)時(shí)會(huì)發(fā)送ScrollNotification類(lèi)型的通知,ScrollBar正是通過(guò)監(jiān)聽(tīng)滾動(dòng)通知來(lái)實(shí)現(xiàn)的。通過(guò)NotificationListener監(jiān)聽(tīng)滾動(dòng)事件和通過(guò)ScrollController有兩個(gè)主要的不同:
    1. 通過(guò)NotificationListener可以在從可滾動(dòng)組件到widget樹(shù)根之間任意位置都能監(jiān)聽(tīng)。而ScrollController只能和具體的可滾動(dòng)組件關(guān)聯(lián)后才可以。
    2. 收到滾動(dòng)事件后獲得的信息不同;NotificationListener在收到滾動(dòng)事件時(shí),通知中會(huì)攜帶當(dāng)前滾動(dòng)位置和ViewPort的一些信息,而ScrollController只能獲取當(dāng)前滾動(dòng)位置。

在接收到滾動(dòng)事件時(shí),參數(shù)類(lèi)型為ScrollNotification,它包括一個(gè)metrics屬性,它的類(lèi)型是ScrollMetrics,該屬性包含當(dāng)前ViewPort及滾動(dòng)位置等信息:
    1. pixels:當(dāng)前滾動(dòng)位置。
    2. maxScrollExtent:最大可滾動(dòng)長(zhǎng)度。
    3. extentBefore:滑出ViewPort頂部的長(zhǎng)度;此示例中相當(dāng)于頂部滑出屏幕上方的列表長(zhǎng)度。
    4. extentInside:ViewPort內(nèi)部長(zhǎng)度;此示例中屏幕顯示的列表部分的長(zhǎng)度。
    5. extentAfter:列表中未滑入ViewPort部分的長(zhǎng)度;此示例中列表底部未顯示到屏幕范圍部分的長(zhǎng)度。
    6. atEdge:是否滑到了可滾動(dòng)組件的邊界。

示例

import 'package:flutter/material.dart';
class ScrollNotificationTestRoute extends StatefulWidget {
  @override
  _ScrollNotificationTestRouteState createState() =>
      new _ScrollNotificationTestRouteState();
}
class _ScrollNotificationTestRouteState
    extends State<ScrollNotificationTestRoute> {
  String _progress = "0%"; // 保存進(jìn)度百分比
  @override
  Widget build(BuildContext context) {
    return Scrollbar( // 進(jìn)度條
      // 監(jiān)聽(tīng)滾動(dòng)通知
      child: NotificationListener<ScrollNotification>(
        onNotification: (ScrollNotification notification) {
          double progress = notification.metrics.pixels /
              notification.metrics.maxScrollExtent;
          // 重新構(gòu)建
          setState(() {
            _progress = "${(progress * 100).toInt()}%";
          });
          print("BottomEdge: ${notification.metrics.extentAfter == 0}");
          //return true; // 放開(kāi)此行注釋后,進(jìn)度條將失效
        },
        child: Stack(
          alignment: Alignment.center,
          children: <Widget>[
            ListView.builder(
                itemCount: 100,
                itemExtent: 50.0,
                itemBuilder: (context, index) {
                  return ListTile(title: Text("$index"));
                }
            ),
            CircleAvatar(  //顯示進(jìn)度百分比
              radius: 30.0,
              child: Text(_progress),
              backgroundColor: Colors.black54,
            )
          ],
        ),
      ),
    );
  }
}

1. ListView (建議指定itemExtent或prototypeItem)

沿一個(gè)方向線性排列所有子組件。支持基于Sliver的延遲構(gòu)建模型。

/*
  1. ListView中的列表項(xiàng)組件是RenderBox,并不是Sliver。
  2. 一個(gè)ListView中只有一個(gè)Sliver(對(duì)列表項(xiàng)進(jìn)行按需加載),默認(rèn)是SliverList,如果指定了itemExtent,則為SliverFixedExtentList;如果prototypeItem屬性不為空,則為SliverPrototypeExtentList。
  3. 可以通過(guò)ListView.custom自定義列表項(xiàng)生成模型,它需要實(shí)現(xiàn)一個(gè)SliverChildDelegate用來(lái)給ListView生成列表項(xiàng)組件。
  4. 可滾動(dòng)組件的構(gòu)造函數(shù)如果需要一個(gè)列表項(xiàng)Builder則支持基于Sliver的懶加載模型的,反之則不支持。
  5. ListView高度邊界無(wú)法確定時(shí)會(huì)異常
*/
ListView({
  // 可滾動(dòng)widget公共參數(shù)
  Axis scrollDirection = Axis.vertical,    
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  EdgeInsetsGeometry padding,    

  // ListView各個(gè)構(gòu)造函數(shù)的共同參數(shù)  
  double itemExtent,
  Widget? prototypeItem, // 列表項(xiàng)原型
  bool shrinkWrap = false,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,

  // 子widget列表
  // 這種方式適合只有少量的子組件數(shù)量已知且比較少的情況,反之則應(yīng)該使用ListView.builder 按需動(dòng)態(tài)構(gòu)建列表項(xiàng)。
  List<Widget> children = const <Widget>[],
})

說(shuō)明:
1. itemExtent
  如果不為null,則表示滾動(dòng)方向上子組件的長(zhǎng)度。
  指定后滾動(dòng)系統(tǒng)可以提前知道列表的長(zhǎng)度,而無(wú)需每次構(gòu)建子組件時(shí)都去再計(jì)算,會(huì)更加高效。
2. prototypeItem(列表項(xiàng)原型)
  如果所有列表項(xiàng)長(zhǎng)度相同但不知道具體多少,可以指定一個(gè)列表項(xiàng)prototypeItem,可滾動(dòng)組件會(huì)在layout時(shí)計(jì)算一次它延主軸方向的長(zhǎng)度,和指定itemExtent一樣。
  注意:itemExtent和prototypeItem互斥,不能同時(shí)指定。
3. shrinkWrap
  是否根據(jù)子組件的總長(zhǎng)度來(lái)設(shè)置ListView的長(zhǎng)度。
  默認(rèn)false ,ListView的會(huì)在滾動(dòng)方向盡可能多的占用空間。當(dāng)ListView在一個(gè)無(wú)邊界(滾動(dòng)方向上)的容器中時(shí),shrinkWrap必須為true。
4. addAutomaticKeepAlives
  是否將列表項(xiàng)(子組件)包裹在AutomaticKeepAlive 組件中;
  如果設(shè)置為true(默認(rèn)為true,在懶加載列表中會(huì)為每一個(gè)列表項(xiàng)添加AutomaticKeepAlive父組件),在列表項(xiàng)滑出視口時(shí)不會(huì)被GC(垃圾回收),它會(huì)使用KeepAliveNotification來(lái)保存其狀態(tài)。
  如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false。
5. addRepaintBoundaries
  是否將列表項(xiàng)(子組件)包裹在RepaintBoundary組件中。默認(rèn)為true。
  將列表項(xiàng)包裹在RepaintBoundary中可以在滾動(dòng)時(shí)避免列表項(xiàng)重繪,但是當(dāng)列表項(xiàng)重繪的開(kāi)銷(xiāo)非常小時(shí),不添加RepaintBoundary反而會(huì)更高效。
  如果列表項(xiàng)自己維護(hù)其KeepAlive狀態(tài),那么此參數(shù)必須置為false

示例

ListView(
  children: [
    imgSection,
    titleSection,
    buttonSection,
    textSection,
  ],
),
ListView(
  padding: EdgeInsets.all(10),
  children: [
    ListTitle(
      title:Text('hello'),
      subTitle:('world'),
    ),
    ListTitle(
      leading:Icon(Icons.settings,color:Colors.yellow,size:30),
      trailing:Image.network("http://.../1.png"),
      title:Text(
          'hello',
           style: TextStyle(
              fontSize: 24,
           ),
      ),
      subTitle:('world'),
    ),
  ],
),
  1. 默認(rèn)構(gòu)造函數(shù)

有一個(gè)children參數(shù),子組件很少時(shí)使用。不支持基于Sliver的懶加載模型。
通過(guò)此方式創(chuàng)建的ListView和使用SingleChildScrollView+Column的方式?jīng)]有本質(zhì)的區(qū)別。

示例

ListView(
  shrinkWrap: true, 
  padding: const EdgeInsets.all(20.0),
  children: <Widget>[
    const Text('I\'m dedicating every day to you'),
    const Text('Domestic life was never quite my style'),
    const Text('When you smile, you knock me out, I fall apart'),
    const Text('And I thought I was so smart'),
  ],
);

示例2

import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'Basic List';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new ListView(
          children: <Widget>[
            new ListTile(
              leading: new Icon(Icons.map),
              title: new Text('Map'),
            ),
            new ListTile(
              leading: new Icon(Icons.photo),
              title: new Text('Album'),
            ),
            new ListTile(
              leading: new Icon(Icons.phone),
              title: new Text('Phone'),
            ),
          ],
        ),
      ),
    );
  }
}

示例3(水平滾動(dòng))

import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'Horizontal List';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new Container(
          margin: new EdgeInsets.symmetric(vertical: 20.0),
          height: 200.0,
          child: new ListView(
            scrollDirection: Axis.horizontal,    // 水平滾動(dòng)
            children: <Widget>[
              new Container(
                width: 260.0,
                color: Colors.red,
              ),
              new Container(
                width: 260.0,
                color: Colors.blue,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
  1. ListView.builder 構(gòu)造函數(shù)

適合列表項(xiàng)比較多或不確定時(shí)。支持基于Sliver的懶加載模型的。

ListView.builder({
  ...
  // 列表項(xiàng)的構(gòu)建器,返回值為一個(gè)widget。當(dāng)滾動(dòng)到對(duì)應(yīng)index位置時(shí)會(huì)調(diào)用。
  @required IndexedWidgetBuilder itemBuilder,
  // 列表項(xiàng)的數(shù)量,如果為null,則為無(wú)限列表。
  int itemCount,
})

示例

ListView.builder(
    itemCount: 100,
    itemExtent: 50.0, // 高度為50.0
    itemBuilder: (BuildContext context, int index) {
      return ListTile(title: Text("$index"));
    }
);

示例2

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
  runApp(new MyApp(
    items: new List<String>.generate(10000, (i) => "Item $i"),
  ));
}
class MyApp extends StatelessWidget {
  final List<String> items;  // 數(shù)據(jù)源
  MyApp({Key key, @required this.items}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final title = 'Long List';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return new ListTile(
              title: new Text('${items[index]}'),
            );
          },
        ),
      ),
    );
  }
}

示例3(不同類(lèi)型的item)

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
  runApp(new MyApp(
    items: new List<ListItem>.generate(
      1000,
      (i) => i % 6 == 0
          ? new HeadingItem("Heading $i")
          : new MessageItem("Sender $i", "Message body $i"),
    ),
  ));
}
class MyApp extends StatelessWidget {
  final List<ListItem> items;
  MyApp({Key key, @required this.items}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    final title = 'Mixed List';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            final item = items[index];
            if (item is HeadingItem) {
              return new ListTile(
                title: new Text(
                  item.heading,
                  style: Theme.of(context).textTheme.headline,
                ),
              );
            } else if (item is MessageItem) {
              return new ListTile(
                title: new Text(item.sender),
                subtitle: new Text(item.body),
              );
            }
          },
        ),
      ),
    );
  }
}
abstract class ListItem {}
class HeadingItem implements ListItem {
  final String heading;
  HeadingItem(this.heading);
}
class MessageItem implements ListItem {
  final String sender;
  final String body;
  MessageItem(this.sender, this.body);
}
  1. ListView.separated

比ListView.builder多了一個(gè)separatorBuilder參數(shù)(在生成的列表項(xiàng)之間添加分割組件)。

示例(奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條綠色下劃線)

class ListView3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget divider1=Divider(color: Colors.blue,);
    Widget divider2=Divider(color: Colors.green);
    return ListView.separated(
        itemCount: 100,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("$index"));
        },
        separatorBuilder: (BuildContext context, int index) {  // 分割器構(gòu)造器
          return index%2==0?divider1:divider2;
        },
    );
  }
}

示例2

從數(shù)據(jù)源異步分批拉取一些數(shù)據(jù),然后用ListView展示,當(dāng)滑動(dòng)到列表末尾時(shí),判斷是否需要再去拉取數(shù)據(jù),如果是,則去拉取,拉取過(guò)程中在表尾顯示一個(gè)loading,拉取成功后將數(shù)據(jù)插入列表;如果不需要再去拉取,則在表尾提示"沒(méi)有更多"

class InfiniteListView extends StatefulWidget {
  @override
  _InfiniteListViewState createState() => new _InfiniteListViewState();
}
class _InfiniteListViewState extends State<InfiniteListView> {
  static const loadingTag = "##loading##"; //表尾標(biāo)記
  var _words = <String>[loadingTag];
  @override
  void initState() {
    super.initState();
    _retrieveData();
  }
  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: _words.length,
      itemBuilder: (context, index) {
        // 如果到了表尾
        if (_words[index] == loadingTag) {
          //不足100條,繼續(xù)獲取數(shù)據(jù)
          if (_words.length - 1 < 100) {
            // 獲取數(shù)據(jù)
            _retrieveData();
            // 加載時(shí)顯示loading
            return Container(
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.center,
              child: SizedBox(
                  width: 24.0,
                  height: 24.0,
                  child: CircularProgressIndicator(strokeWidth: 2.0)
              ),
            );
          } else {
            // 已經(jīng)加載了100條數(shù)據(jù),不再獲取數(shù)據(jù)。
            return Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16.0),
                child: Text("沒(méi)有更多了", style: TextStyle(color: Colors.grey),)
            );
          }
        }
        // 顯示單詞列表項(xiàng)
        return ListTile(title: Text(_words[index]));
      },
      separatorBuilder: (context, index) => Divider(height: .0),
    );
  }
  void _retrieveData() {
    Future.delayed(Duration(seconds: 2)).then((e) {
      setState(() {
        // 重新構(gòu)建列表
        _words.insertAll(_words.length - 1,
          // 每次生成20個(gè)單詞
          generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
          );
      });
    });
  }
}

  1. 添加固定的列表頭
不太好的寫(xiě)法:
@override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("商品列表")),
    SizedBox(
      // Material設(shè)計(jì)規(guī)范中狀態(tài)欄、導(dǎo)航欄、ListTile高度分別為24、56、56 。避免底部留白
      height: MediaQuery.of(context).size.height-24-56-56,
      child: ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      }),
  )
  ]);
}
這種方法太不好,如果頁(yè)面布局發(fā)生變化,比如表頭布局調(diào)整導(dǎo)致表頭高度改變,那么剩余空間的高度就得重新計(jì)算。修正:
// 自動(dòng)拉伸ListView以填充屏幕剩余空間
@override
Widget build(BuildContext context) {
  return Column(children: <Widget>[
    ListTile(title:Text("商品列表")),
    Expanded(
      child: ListView.builder(itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("$index"));
      }),
    ),
  ]);
}
  1. AutomaticKeepAlive組件
將列表項(xiàng)的根RenderObject的keepAlive按需自動(dòng)標(biāo)記為true或false。
列表組件的Viewport區(qū)域+cacheExtent預(yù)渲染區(qū)域 稱(chēng)為加載區(qū)域 :
    1. 當(dāng) keepAlive 標(biāo)記為 false 時(shí),如果列表項(xiàng)滑出加載區(qū)域時(shí),列表組件將會(huì)被銷(xiāo)毀。
    2. 當(dāng) keepAlive 標(biāo)記為 true 時(shí),當(dāng)列表項(xiàng)滑出加載區(qū)域后,Viewport 會(huì)將列表組件緩存起來(lái);當(dāng)列表項(xiàng)進(jìn)入加載區(qū)域時(shí),Viewport 從先從緩存中查找是否已經(jīng)緩存,如果有則直接復(fù)用,如果沒(méi)有則重新創(chuàng)建列表項(xiàng)。

子組件想改變是否需要緩存的狀態(tài)時(shí)就向KeepAliveNotification通知,AutomaticKeepAlive收到消息后會(huì)去更改keepAlive的狀態(tài)(從true變?yōu)閒alse時(shí),需要釋放緩存)。
  1. 優(yōu)化ListView
1. 
列表項(xiàng)較多或不確定(上拉加載更多)時(shí)不要使用默認(rèn)的構(gòu)造函數(shù),應(yīng)該使用ListView.builder
2. 
禁用addAutomaticKeepAlives(缺點(diǎn):滑動(dòng)過(guò)快時(shí)可能會(huì)出現(xiàn)短暫白屏)。
禁用addRepaintBoundaries,當(dāng)列表元素布局較簡(jiǎn)單時(shí)可提高流暢度。
3.
列表中不可變子組件使用const修飾。 // children: [const ListImage()],
4.
指定itemExtent值(當(dāng)可以提前知道時(shí))。
  1. AnimatedList(在列表中插入或刪除節(jié)點(diǎn)時(shí)執(zhí)行一個(gè)動(dòng)畫(huà))
AnimatedList(StatefulWidget類(lèi)型)對(duì)應(yīng)的State類(lèi)型為AnimatedListState(包含了添加和刪除元素的方法):
  void insertItem(int index, { Duration duration = _kDuration });
  void removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration = _kDuration }) ;

要使用上面的添加和刪除方法則需要?jiǎng)?chuàng)建GlobalKey并賦值給AnimatedList的key,通過(guò)key.currentState獲取到AnimatedListState對(duì)象來(lái)調(diào)用。
  final globalKey = GlobalKey<AnimatedListState>();
  AnimatedList(key: globalKey, ...)
  globalKey.currentState.insertItem(data.length - 1);

在插入和刪除數(shù)據(jù)時(shí),應(yīng)該是先修改列表數(shù)據(jù),然后調(diào)用 AnimatedListState 的insertItem 和 removeItem 方法,而不能直接操作完數(shù)據(jù)后刷新界面。

示例(AnimatedList)

點(diǎn)擊底部 + 按鈕時(shí)向列表追加一個(gè)列表項(xiàng);點(diǎn)擊每個(gè)列表項(xiàng)后面的刪除按鈕時(shí),刪除該列表項(xiàng),添加和刪除時(shí)分別執(zhí)行指定的動(dòng)畫(huà)(漸顯、漸隱+收縮)。

class AnimatedListRoute extends StatefulWidget {
  const AnimatedListRoute({Key? key}) : super(key: key);
  @override
  _AnimatedListRouteState createState() => _AnimatedListRouteState();
}
class _AnimatedListRouteState extends State<AnimatedListRoute> {
  var data = <String>[];
  int counter = 5;
  final globalKey = GlobalKey<AnimatedListState>();
  @override
  void initState() {
    for (var i = 0; i < counter; i++) {
      data.add('${i + 1}');
    }
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 與ListView的itemBuilder相比多了一個(gè)animation參數(shù)
        // typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index,Animation<double> animation);
        AnimatedList(
          key: globalKey,  
          initialItemCount: data.length,
          itemBuilder: (  
            BuildContext context,
            int index,
            Animation<double> animation,
          ) {
            // 添加列表項(xiàng)時(shí)會(huì)執(zhí)行漸顯動(dòng)畫(huà)
            return FadeTransition(
              opacity: animation,
              child: buildItem(context, index),
            );
          },
        ),
        buildAddBtn(),
      ],
    );
  }
  // 創(chuàng)建一個(gè) “+” 按鈕,點(diǎn)擊后會(huì)向列表中插入一項(xiàng)
  Widget buildAddBtn() {
    return Positioned(
      child: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          // 添加一個(gè)列表項(xiàng)
          data.add('${++counter}');
          // 告訴列表項(xiàng)有新添加的列表項(xiàng)
          globalKey.currentState!.insertItem(data.length - 1);
          print('添加 $counter');
        },
      ),
      bottom: 30,
      left: 0,
      right: 0,
    );
  }
  // 構(gòu)建列表項(xiàng)
  Widget buildItem(context, index) {
    String char = data[index];
    return ListTile(
      // 數(shù)字不會(huì)重復(fù),所以作為Key
      key: ValueKey(char),
      title: Text(char),
      trailing: IconButton(
        icon: Icon(Icons.delete),
        // 點(diǎn)擊時(shí)刪除
        onPressed: () => onDelete(context, index),
      ),
    );
  }
  void onDelete(context, index) {
    setState(() {
      globalKey.currentState!.removeItem(
        index,
            (context, animation) {
          // 刪除過(guò)程執(zhí)行的是反向動(dòng)畫(huà),animation.value 會(huì)從1變?yōu)?
          var item = buildItem(context, index);
          print('刪除 ${data[index]}');
          data.removeAt(index);
          // 刪除動(dòng)畫(huà)是一個(gè)合成動(dòng)畫(huà):漸隱 + 縮小列表項(xiàng)告訴
          return FadeTransition(
            opacity: CurvedAnimation(
              parent: animation,
              // 讓透明度變化的更快一些
              curve: const Interval(0.5, 1.0),
            ),
            // 不斷縮小列表項(xiàng)的高度
            child: SizeTransition(
              sizeFactor: animation,
              axisAlignment: 0.0,
              child: item,
            ),
          );
        },
        duration: Duration(milliseconds: 200), // 動(dòng)畫(huà)時(shí)間為 200 ms
      );
    });
  }
}

示例

AnimatedList顯示與ListModel保持同步的卡片列表。當(dāng)新的item被添加到ListModel或從ListModel中刪除時(shí),相應(yīng)的卡片在UI上也會(huì)被添加或刪除,并伴有動(dòng)畫(huà)效果。
點(diǎn)擊一個(gè)item選擇它,再次點(diǎn)擊它會(huì)取消選擇。點(diǎn)擊’+’插入選定的item,點(diǎn)擊’ - ‘刪除選定的item。 tap處理器會(huì)從ListModel<E>中添加或刪除items,ListModel<E>是List<E>的簡(jiǎn)單封裝 ,用于保持和AnimatedList的同步。 列表模型為其動(dòng)畫(huà)列表提供了一個(gè)GlobalKey。它使用該鍵來(lái)調(diào)用由AnimatedListState定義的insertItem和removeItem方法。

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AnimatedListSample extends StatefulWidget {
  @override
  _AnimatedListSampleState createState() => new _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample> {
  // 由于AnimatedList的所有控制都是在AnimatedState中進(jìn)行的。在構(gòu)建AnimatedList時(shí)給key屬性賦值GlobalKey,就可以通過(guò)key.currentState獲取到AnimatedListState對(duì)象。
  final GlobalKey<AnimatedListState> _listKey = new GlobalKey<AnimatedListState>();
  ListModel<int> _list;
  int _selectedItem;
  int _nextItem; 
  @override
  void initState() {
    super.initState();
    _list = new ListModel<int>(
      listKey: _listKey,
      initialItems: <int>[0, 1, 2],
      removedItemBuilder: _buildRemovedItem,
    );
    _nextItem = 3;
  }
  // 構(gòu)建列表項(xiàng)(沒(méi)有被移除的)
  Widget _buildItem(BuildContext context, int index, Animation<double> animation) {
    return new CardItem(
      animation: animation,
      item: _list[index],
      selected: _selectedItem == _list[index],
      onTap: () {
        setState(() {
          _selectedItem = _selectedItem == _list[index] ? null : _list[index];
        });
      },
    );
  }
  // 
  Widget _buildRemovedItem(int item, BuildContext context, Animation<double> animation) {
    return new CardItem(
      animation: animation,
      item: item,
      selected: false,
    );
  }
  // 插入
  void _insert() {
    final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
    _list.insert(index, _nextItem++);
  }
  // 移除選中
  void _remove() {
    if (_selectedItem != null) {
      _list.removeAt(_list.indexOf(_selectedItem));
      setState(() {
        _selectedItem = null;
      });
    }
  }
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('AnimatedList'),
          actions: <Widget>[
            new IconButton(
              icon: const Icon(Icons.add_circle),
              onPressed: _insert,
              tooltip: 'insert a new item',
            ),
            new IconButton(
              icon: const Icon(Icons.remove_circle),
              onPressed: _remove,
              tooltip: 'remove the selected item',
            ),
          ],
        ),
        body: new Padding(
          padding: const EdgeInsets.all(16.0),
          child: new AnimatedList(
            key: _listKey,
            initialItemCount: _list.length,
            itemBuilder: _buildItem,
          ),
        ),
      ),
    );
  }
}
// 
class ListModel<E> {
  ListModel({
    @required this.listKey,
    @required this.removedItemBuilder,
    Iterable<E> initialItems,
  }) : assert(listKey != null),
       assert(removedItemBuilder != null),
       _items = new List<E>.from(initialItems ?? <E>[]);
  final GlobalKey<AnimatedListState> listKey;
  final dynamic removedItemBuilder;
  final List<E> _items;
  AnimatedListState get _animatedList => listKey.currentState;
  void insert(int index, E item) {
    _items.insert(index, item);
    // insertItem 方法沒(méi)有 builder 參數(shù),它直接將新插入的元素傳給 AnimatedList 的 builder 方法來(lái)插入新的元素,這樣能夠保持和列表新增元素的動(dòng)效一致。
    _animatedList.insertItem(index);
  }
  E removeAt(int index) {
    final E removedItem = _items.removeAt(index);
    if (removedItem != null) {
      // 傳入?yún)?shù):移除元素的下標(biāo) 和 一個(gè)構(gòu)建移除元素的方法builder。之所以要這個(gè)方法是因?yàn)樵貙?shí)際從列表馬上移除的,為了在動(dòng)畫(huà)過(guò)渡時(shí)間內(nèi)還能夠看到被移除的元素,需要通過(guò)這種方式來(lái)構(gòu)建一個(gè)被移除的元素來(lái)感覺(jué)是動(dòng)畫(huà)刪除的。這里也可以使用 animation 參數(shù)自定義動(dòng)畫(huà)效果。 
      _animatedList.removeItem(index, (BuildContext context, Animation<double> animation) {
        return removedItemBuilder(removedItem, context, animation);
      });
    }
    return removedItem;
  }
  int get length => _items.length;
  E operator [](int index) => _items[index];
  int indexOf(E item) => _items.indexOf(item);
}
// 列表項(xiàng)
class CardItem extends StatelessWidget {
  const CardItem({
    Key key,
    @required this.animation,
    this.onTap,
    @required this.item,
    this.selected: false
  }) : assert(animation != null),
       assert(item != null && item >= 0),
       assert(selected != null),
       super(key: key);
  final Animation<double> animation;
  final VoidCallback onTap;
  final int item;
  final bool selected;
  @override
  Widget build(BuildContext context) {
    TextStyle textStyle = Theme.of(context).textTheme.display1;
    if (selected)
      textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
    return new Padding(
      padding: const EdgeInsets.all(2.0),
      child: new SizeTransition(
        axis: Axis.vertical,
        sizeFactor: animation,
        child: new GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: onTap,
          child: new SizedBox(
            height: 128.0,
            child: new Card(
              color: Colors.primaries[item % Colors.primaries.length],
              child: new Center(
                child: new Text('Item $item', style: textStyle),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
void main() {
  runApp(new AnimatedListSample());
}

2. SingleChildScrollView (只能接收一個(gè)子組件)

內(nèi)容不會(huì)超過(guò)屏幕太多時(shí)使用,不支持基于Sliver的延遲實(shí)例化模型。

  SingleChildScrollView({
    // 公共參數(shù)
    Key? key,
    this.scrollDirection = Axis.vertical,  // 
    this.reverse = false,  // 
    this.padding,  //
    bool? primary,  // 
    this.physics,  // 
    this.controller,  // 
    this.child,  //
    // 
    this.dragStartBehavior = DragStartBehavior.start,
    this.clipBehavior = Clip.hardEdge,
    this.restorationId,
  }) 

示例(將大寫(xiě)字母A-Z沿垂直方向顯示)

class SingleChildScrollViewTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return Scrollbar( // 顯示進(jìn)度條
      child: SingleChildScrollView(
        padding: EdgeInsets.all(16.0),
        child: Center(
          child: Column( 
            // 動(dòng)態(tài)創(chuàng)建一個(gè)List<Widget>  
            children: str.split("") 
                // 每一個(gè)字母都用一個(gè)Text顯示,字體為原來(lái)的兩倍
                .map((c) => Text(c, textScaleFactor: 2.0,)) 
                .toList(),
          ),
        ),
      ),
    );
  }
}
運(yùn)行結(jié)果

3. GridView(二維網(wǎng)格列表)

GridView({
  Axis scrollDirection = Axis.vertical,    // 滾動(dòng)方向
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,  // 內(nèi)邊距
  // 控制子組件如何排列
  // SliverGridDelegate定義了GridView Layout相關(guān)接口,子類(lèi)需要通過(guò)實(shí)現(xiàn)它們來(lái)實(shí)現(xiàn)具體的布局算法。
  // SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。
  @required SliverGridDelegate gridDelegate, 
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],    // 子列表
})
和ListView的大多數(shù)參數(shù)都是相同的。
  1. SliverGridDelegateWithFixedCrossAxisCount、GridView.count

橫軸為固定數(shù)量子元素。

// GridView.count構(gòu)造函數(shù)內(nèi)部使用了SliverGridDelegateWithFixedCrossAxisCount。
SliverGridDelegateWithFixedCrossAxisCount({
/*
  // 橫軸子元素的數(shù)量。
  // 子元素在橫軸的長(zhǎng)度=ViewPort橫軸長(zhǎng)度/crossAxisCount。
  // 子元素的大小是通過(guò)crossAxisCount和childAspectRatio兩個(gè)參數(shù)共同決定的。這里的子元素指的是子組件的最大顯示空間,確保子組件的實(shí)際大小不要超出子元素的空間。
*/
  @required double crossAxisCount, 
   // 主軸方向的間距
  double mainAxisSpacing = 0.0,
  // 橫軸方向的間距。
  double crossAxisSpacing = 0.0,
  // 子元素在橫軸長(zhǎng)度和主軸長(zhǎng)度的比例。
  double childAspectRatio = 1.0,
})

示例

GridView(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3, // 橫軸三個(gè)子widget
      childAspectRatio: 1.0 // 寬高比為1
  ),
  children:<Widget>[
    Icon(Icons.ac_unit),
    Icon(Icons.airport_shuttle),
    Icon(Icons.all_inclusive),
    Icon(Icons.beach_access),
    Icon(Icons.cake),
    Icon(Icons.free_breakfast)
  ]
);
上面的示例代碼等價(jià)于(GridView.count):
GridView.count( 
  crossAxisCount: 3,
  childAspectRatio: 1.0,
  children: <Widget>[
    Icon(Icons.ac_unit),
    Icon(Icons.airport_shuttle),
    Icon(Icons.all_inclusive),
    Icon(Icons.beach_access),
    Icon(Icons.cake),
    Icon(Icons.free_breakfast),
  ],
);

示例(GridView.count)

import 'package:flutter/material.dart';
void main() {
  runApp(new MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final title = 'Grid List';
    return new MaterialApp(
      title: title,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(title),
        ),
        body: new GridView.count(
          crossAxisCount: 2,
          children: new List.generate(100, (index) {
            return new Center(
              child: new Text(
                'Item $index',
                style: Theme.of(context).textTheme.headline,
              ),
            );
          }),
        ),
      ),
    );
  }
}
  1. SliverGridDelegateWithMaxCrossAxisExtent、GridView.extent

橫軸子元素為固定最大長(zhǎng)度。

// GridView.extent構(gòu)造函數(shù)內(nèi)部使用了SliverGridDelegateWithMaxCrossAxisExtent。
SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent,    // 子元素在橫軸上的最大長(zhǎng)度
  double mainAxisSpacing = 0.0,
  double crossAxisSpacing = 0.0,
  double childAspectRatio = 1.0,
})

示例

GridView(
  padding: EdgeInsets.zero,
  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 120.0,
      childAspectRatio: 2.0 // 寬高比為2
  ),
  children: <Widget>[
    Icon(Icons.ac_unit),
    Icon(Icons.airport_shuttle),
    Icon(Icons.all_inclusive),
    Icon(Icons.beach_access),
    Icon(Icons.cake),
    Icon(Icons.free_breakfast),
  ],
);
上面的示例代碼等價(jià)于:
GridView.extent(
   maxCrossAxisExtent: 120.0,
   childAspectRatio: 2.0,
   children: <Widget>[
     Icon(Icons.ac_unit),
     Icon(Icons.airport_shuttle),
     Icon(Icons.all_inclusive),
     Icon(Icons.beach_access),
     Icon(Icons.cake),
     Icon(Icons.free_breakfast),
   ],
 );
  1. GridView.builder

通過(guò)GridView.builder來(lái)動(dòng)態(tài)創(chuàng)建子widget。

GridView.builder(
 ...
 @required SliverGridDelegate gridDelegate, 
 @required IndexedWidgetBuilder itemBuilder,  // 子widget構(gòu)建器
)

示例

從一個(gè)異步數(shù)據(jù)源(如網(wǎng)絡(luò))分批獲取一些Icon,然后用GridView來(lái)展示

class InfiniteGridView extends StatefulWidget {
  @override
  _InfiniteGridViewState createState() => new _InfiniteGridViewState();
}
class _InfiniteGridViewState extends State<InfiniteGridView> {
  List<IconData> _icons = []; //保存Icon數(shù)據(jù)
  @override
  void initState() {
    // 初始化數(shù)據(jù)  
    _retrieveIcons();
  }
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
            // mainAxisSpacing: 10.0,
            // crossAxisSpacing: 10.0,
            crossAxisCount: 3, // 每行三列
            childAspectRatio: 1.0 // 顯示區(qū)域?qū)捀呦嗟?        ),
        itemCount: _icons.length,
        itemBuilder: (context, index) {
          // 如果顯示到最后一個(gè)并且Icon總數(shù)小于200時(shí)繼續(xù)獲取數(shù)據(jù)
          if (index == _icons.length - 1 && _icons.length < 200) {
            _retrieveIcons();
          }
          return Icon(_icons[index]);
        }
    );
  }
  void _retrieveIcons() {
    Future.delayed(Duration(milliseconds: 200)).then((e) {
      setState(() {
        _icons.addAll([
          Icons.ac_unit,
          Icons.airport_shuttle,
          Icons.all_inclusive,
          Icons.beach_access, Icons.cake,
          Icons.free_breakfast
        ]);
      });
    });
  }
}
flutter_staggered_grid_view包可實(shí)現(xiàn)這種布局
最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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