七、Flutter_List組件

一、ListView組件

Android中,我們可以使用ListViewRecyclerView來實(shí)現(xiàn),在iOS中,我們可以通過UITableView來實(shí)現(xiàn)。

Flutter中,我們也有對(duì)應(yīng)的列表Widget,就是ListView

1.1 ListView基礎(chǔ)

ListView的內(nèi)部繼承順序:

ListView extends BoxScrollView —> extends ScrollView —> extends StatelessWidget

在ListView中,有4種構(gòu)造方法:

  • ListView<Widget>,適合于具有少量子元素的列表視圖
  • ListView.builder,利用IndexedWidgetBuilder來按需構(gòu)造.適合于具有大量子視圖的列表視圖,構(gòu)建器只對(duì)那些實(shí)際可見的子視圖調(diào)用
  • ListView.separated,采用兩個(gè)IndexedWidgetBuilder:itemBuilder根據(jù)需要構(gòu)建子項(xiàng)separatorBuilder類似地構(gòu)建出現(xiàn)在子項(xiàng)之間的分隔符子項(xiàng)。適用于具有固定數(shù)量的子控件的列表視圖
  • ListView.custom,使用SliverChildDelegate構(gòu)造,它提供了定制子模型的其他方面的能力。 例如,SliverChildDelegate可以控制用于估計(jì)實(shí)際上不可見的孩子的大小的算法

1.1.1 ListView<Widget>

ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget

最簡(jiǎn)單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性

class MyHomeBody extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("人的一切痛苦,本質(zhì)上都是對(duì)自己無能的憤怒。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("人活在世界上,不可以有偏差;而且多少要費(fèi)點(diǎn)勁兒,才能把自己保持到理性的軌道上。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("我活在世上,無非想要明白些道理,遇見些有趣的事。", style: textStyle),
        )
      ],
    );
  }
}
1.1.1.1 ListTile的使用

類似通訊錄的列表,我們可以通過ListTile類實(shí)現(xiàn)

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

  @override
  Widget build(BuildContext context) {
    return ListView(
      scrollDirection: Axis.vertical,
      children: List.generate(255, (index) {
        return ListTile(
          leading: Icon(Icons.favorite),
          trailing: Icon(Icons.pets),
          title: Text("聯(lián)系人 ${index + 1}"),
          subtitle: Text("聯(lián)系人電話號(hào)碼"),
        );
      })
    );
  }
}
1.1.1.2 比較重要的屬性 scrollDirection、 itemExtent

scrollDirection:控制列表的滾動(dòng)方向

itemExtent:設(shè)置每一個(gè)item的高度(如果是Axis.horizontal,則為寬度)

reverse:翻轉(zhuǎn)屬性,默認(rèn)為false(從最底部開始排列)

更多的看源碼,嘗試。不做過多介紹

通過上面的兩個(gè)示例,想必你已知曉。默認(rèn)會(huì)創(chuàng)建出所有的childWidget,這樣無疑會(huì)增加性能的開銷. 對(duì)于更多數(shù)量未知的情況,并不適用

1.1.2 ListView.builder

ListView.builder方法有兩個(gè)重要的參數(shù):

  • itemBuilder(必傳) 按需構(gòu)造
  • itemCount 數(shù)量
class ListViewBuilderDemo extends StatelessWidget {
  const ListViewBuilderDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemBuilder:(BuildContext context, int index){
          return ListTile(title: Text("標(biāo)題$index"), subtitle: Text("詳情內(nèi)容$index"));
        },
      itemCount: 20,
      itemExtent: 30,
    );
  }
}

1.1.3 ListView.separated

ListView.separated可以生成列表項(xiàng)之間的分割器,它除了比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割器生成器

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

class MySeparatedDemo extends StatelessWidget {
  Divider blueColor = Divider(color: Colors.blue);
  Divider redColor = Divider(color: Colors.red);

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          leading: Icon(Icons.people),
          title: Text("聯(lián)系人${index+1}"),
          subtitle: Text("聯(lián)系人電話${index+1}"),
        );
      },
      separatorBuilder: (BuildContext context, int index) {
        return index % 2 == 0 ? redColor : blueColor;
      },
      itemCount: 100
    );
  }
}

示例2:在指定區(qū)域內(nèi),以Icon為分隔器

class ListViewSeparatedDemo extends StatelessWidget {
  const ListViewSeparatedDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 300,
      child: ListView.separated(
        itemBuilder: (BuildContext ctx, int index) {
          return Container(
            height: 40,
            child: Padding(
              padding: const EdgeInsets.only(left: 20),
              child: Text(
                "helloworld $index",
                style: TextStyle(fontSize: 20),
              ),
            ),
          );
        },
        separatorBuilder: (BuildContext ctx, int index) {
          return Icon(Icons.pets,size: 40,);
        },
        itemCount: 100,
      ),
    );
  }
}

二、GridView組件

iOS中,我們可以通過UICollectionView來實(shí)現(xiàn)多列。在Flutter中也有對(duì)應(yīng)的列表Widget,就是GridView,使用方式和ListView也比較相似。

2.1 GridView基礎(chǔ)

GridView的內(nèi)部繼承順序:

GridView extends BoxScrollView —> extends ScrollView —> extends StatelessWidget

可以對(duì)比得知,GridView與ListView繼承于BoxScrollView,所以在很多方面二者是極其相似的

在GridView中,有4種構(gòu)造方法:

  • GridView<Widget>,相對(duì)于ListView多gridDelegate這個(gè)非常特殊的參數(shù)
  • GridView.count,GridView.extent(類比上面,可以不用設(shè)置delegate)
  • GridView.builder,
  • GridView.custom,

2.1.1 GridView<Widget>

gridDelegate:控制交叉軸的item數(shù)量或者寬度,需要傳入的類型是SliverGridDelegate

SliverGridDelegate是一個(gè)抽象類,我們找到它的兩個(gè)子類:

  • SliverGridDelegateWithFixedCrossAxisCount控制交叉軸的item數(shù)量
  • SliverGridDelegateWithMaxCrossAxisExtent控制交叉軸的item的最大寬度

SliverGridDelegateWithFixedCrossAxisCount:包含參數(shù)

@required this.crossAxisCount,//
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,

SliverGridDelegateWithMaxCrossAxisExtent:包含參數(shù)

@required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,

代碼演示:

//SliverGridDelegateWithMaxCrossAxisExtent示例
class GridViewDelegateDemo extends StatelessWidget {
  const GridViewDelegateDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
      child: GridView(
        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 200,
          childAspectRatio: 1.5,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        children: List.generate(100, (index) {
          return Container(
            color: Color.fromARGB(255, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256)),
          );
        }),
      ),
    );
  }
}

//SliverGridDelegateWithFixedCrossAxisCount示例
class GridViewDemo extends StatelessWidget {
  const GridViewDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5),
      child: GridView(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          childAspectRatio: 15 / 9.0,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
        ),
        children: List.generate(100, (index) {
          return Container(
            color: Color.fromARGB(255, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256)),
          );
        }),
      ),
    );
  }
}

2.1.2 GridView.count, GridView.extent

??上面這兩個(gè)構(gòu)造函數(shù),有這對(duì)應(yīng)的簡(jiǎn)寫方式,即GridView.count, GridView.extent構(gòu)造函數(shù)內(nèi)部實(shí)現(xiàn)了對(duì)應(yīng)的delegate

沒有什么好講的,直接上代碼:

class GridViewCountDemo extends StatelessWidget {
  const GridViewCountDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
      child: GridView.count(
        crossAxisCount: 3,
        crossAxisSpacing: 10,
        mainAxisSpacing: 10,
        childAspectRatio: 0.9,
        children: List.generate(100, (index) {
          return Container(
            color: Color.fromARGB(255, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256)),
          );
        }),
      ),
    );
  }
}

class GridViewExtentDemo extends StatelessWidget {
  const GridViewExtentDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
        child: GridView.extent(
          maxCrossAxisExtent: 200,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
          childAspectRatio: 0.9,
          children: List.generate(100, (index) {
            return Container(
              color: Color.fromARGB(255, Random().nextInt(256),
                  Random().nextInt(256), Random().nextInt(256)),
            );
          }),
        ));
  }
}

2.1.3 GridView.builder

類似ListView.builder,可以使用GridView.build來交給GridView自己管理需要?jiǎng)?chuàng)建的子Widget,降低性能消耗

class GrideViewBuilderDemo extends StatelessWidget {
  const GrideViewBuilderDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisSpacing: 8,
          crossAxisSpacing: 8,
        ),
        itemBuilder: (BuildContext ctx, int index) {
          return Container(
            height: 40,
            child: Padding(
              padding: const EdgeInsets.only(left: 20),
              child: Text(
                "helloworld $index",
                style: TextStyle(fontSize: 20),
              ),
            ),
          );
        });
  }
}

2.1.4 GridView.custom

在源碼中,我們可以看到上面的構(gòu)造方法,設(shè)置了SliverChildListDelegate,而GridView.custom則是需要自己去設(shè)置

class GrideViewCustomDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.custom(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
      ),
      childrenDelegate: SliverChildListDelegate(
          List.generate(100, (index) {
            return Container(
                color: Color.fromARGB(255, Random().nextInt(256),
                Random().nextInt(256), Random().nextInt(256))
            );
          }),
          addAutomaticKeepAlives: false,
      ),
    );
  }
}

三、Slivers(裂片)

設(shè)想一下平常很常見的視圖布局:一個(gè)滑動(dòng)的視圖中包括一個(gè)標(biāo)題視圖(HeaderView),一個(gè)列表視圖(ListView),一個(gè)網(wǎng)格視圖(GridView),如何讓它們做到統(tǒng)一滑動(dòng)呢?

Flutter中有一個(gè)可以完成這樣滾動(dòng)效果的Widget:CustomScrollView,可以統(tǒng)一管理多個(gè)滾動(dòng)視圖

CustomScrollView中,每一個(gè)獨(dú)立的,可滾動(dòng)的Widget被稱之為Sliver。

3.1 Slivers的使用

需要通過CustomScrollView來管理Slivers通過slivers屬性,放數(shù)量不定的Sliver:

Sliver的種類:

  • SliverList:類似于我們之前使用過的ListView;
  • SliverGrid:類似于我們之前使用過的GridView;
  • SliverFixedExtentList:類似于SliverList,只是可以設(shè)置item的高度;
  • SliverAppBar:添加一個(gè)AppBar,包裹Slive,作為CustomScrollView的HeaderView;

給Sliver修改一些顯示區(qū)域布局:

  • SliverPadding:包裹Slive,設(shè)置Sliver的內(nèi)邊距;
  • SliverSafeArea:包裹Slive,設(shè)置內(nèi)容顯示安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容)

示例:

class SliverSingleDemo extends StatelessWidget {
  const SliverSingleDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
        slivers: [
          SliverSafeArea(
            sliver: SliverPadding(
              padding: EdgeInsets.only(top: 10,left: 10,right: 10),
              sliver: SliverGrid(
                delegate: SliverChildBuilderDelegate(
                      (BuildContext ctx, int index) {
                    return Container(
                      height: 40,
                      color: Color.fromARGB(255, Random().nextInt(256),
                          Random().nextInt(256), Random().nextInt(256)),
                      child: Padding(
                        padding: const EdgeInsets.only(left: 20),
                        child: Text(
                          "helloworld $index",
                          style: TextStyle(fontSize: 20),
                        ),
                      ),
                    );
                  },
                  childCount: 100,
                ),
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisSpacing: 10,
                    mainAxisSpacing: 10,
                    crossAxisCount: 5,
                    childAspectRatio: 1.5),
              ),
            ),
          )
        ],
      );
  }
}

示例:SliverAppBar + SliverGrid + SliverFixedExtentList + SliverPadding+SliverSafeArea

class MutiSliverDemo extends StatelessWidget {
  const MutiSliverDemo({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverAppBar(
          pinned: true,//懸停效果
          expandedHeight: 300,//高度
          flexibleSpace: FlexibleSpaceBar(//靈活的headview
            title: Text('Sliver demo'),
            background: Image.network('https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg',fit: BoxFit.cover,),
          ),
        ),
        SliverGrid(delegate: SliverChildBuilderDelegate(
              (BuildContext ctx, int index) {
            return Container(
              alignment: Alignment.center,
              color: Colors.teal[100 * (index % 9)],
              /*color: Color.fromARGB(255, Random().nextInt(256),
                  Random().nextInt(256), Random().nextInt(256)),*/
              child: new Text('grid item $index'),
            );
          },
          childCount: 10,
        ), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 200.0,
          mainAxisSpacing: 10.0,
          crossAxisSpacing: 10.0,
          childAspectRatio: 4.0,),
        ),
        SliverFixedExtentList(
            itemExtent: 50,
            delegate: SliverChildBuilderDelegate(
              (BuildContext ctx, int index) {
                return Container(
                  alignment: Alignment.center,
                  color: Colors.lightBlue[100 * (index % 9)],
                  child: new Text('list item $index'),
                );
              },
              childCount: 20,
            ))
      ],
    );
  }
}

SliverAppBar有很多屬性,有時(shí)間可以自己查看源碼及官方文檔,嘗試一下

四、監(jiān)聽滾動(dòng)事件

在Flutter中監(jiān)聽滾動(dòng)相關(guān)的內(nèi)容由兩部分組成:ScrollControllerScrollNotification。

4.1 ScrollController

  1. 在Flutter中,Widget并不是最終渲染到屏幕上的元素(渲染的是RenderObject),因此通常這種監(jiān)聽事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過對(duì)應(yīng)的Widget的Controller來實(shí)現(xiàn)。
  2. 通常情況下,根據(jù)滾動(dòng)的位置來改變一些Widget的狀態(tài)信息,ScrollController通常會(huì)和StatefulWidget一起來使用,并且會(huì)在其中控制它的初始化、監(jiān)聽、銷毀等事件。
  3. ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來監(jiān)聽滾動(dòng)事件。
  4. 手動(dòng)設(shè)置offset通過以下兩個(gè)方法:
  • jumpTo(double offset)animateTo(double offset,...):這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置,不同之處: 后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫,而前者不會(huì)。

案例1:當(dāng)滾動(dòng)到1000位置的時(shí)候,顯示一個(gè)回到頂部的按鈕

class ZQHomePage extends StatefulWidget {
  @override
  _ZQHomePageState createState() => _ZQHomePageState();
}

class _ZQHomePageState extends State<ZQHomePage> {
  ScrollController _controller = ScrollController(initialScrollOffset: 300);
  bool _isShowFloatButton = false;
  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      print("監(jiān)聽滾動(dòng)。。。。${_controller.offset}");
      setState(() {
        _isShowFloatButton = _controller.offset >= 1000;
      });
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('列表測(cè)試'),
      ),
      body: ListView.builder(
        controller: _controller,
        itemBuilder: (BuildContext ctx, int index) {
          return ListTile(
            leading: Icon(Icons.pets),
            title: Text("聯(lián)系人$index"),
          );
        },
        itemCount: 300,
      ),
      floatingActionButton: _isShowFloatButton ? FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: () {
          //controller.jumpTo(0);
          _controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
        },
      ) : null,
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
      floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
    );
  }

4.2 NotificationListener

通過NotificationListener,可以監(jiān)聽什么時(shí)候開始滾動(dòng),什么時(shí)候結(jié)束滾動(dòng)

  • NotificationListener是一個(gè)Widget,模板參數(shù)T是想監(jiān)聽的通知類型,如果省略,則所有類型通知都會(huì)被監(jiān)聽,如果指定特定類型,則只有該類型的通知會(huì)被監(jiān)聽。
  • NotificationListener需要一個(gè)onNotification回調(diào)函數(shù),用于實(shí)現(xiàn)監(jiān)聽處理邏輯。
  • 該回調(diào)可以返回一個(gè)布爾值,代表是否阻止該事件繼續(xù)向上冒泡,如果為true時(shí),則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時(shí),則冒泡繼續(xù)。

案例: 列表滾動(dòng), 并且在中間顯示滾動(dòng)進(jìn)度

class ZQNewHomePage extends StatefulWidget {
  @override
  _ZQNewHomePageState createState() => _ZQNewHomePageState();
}

class _ZQNewHomePageState extends State<ZQNewHomePage> {
  ScrollController _controller = ScrollController(initialScrollOffset: 300);
  bool _isShowFloatButton = false;
  int _progress = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('列表測(cè)試'),
      ),
      body: NotificationListener(
        onNotification: (ScrollNotification notification){
          if(notification is ScrollStartNotification){
            print("開始滾動(dòng)..");

          }else if(notification is ScrollEndNotification){
            print("結(jié)束滾動(dòng)..");
          }else if(notification is ScrollUpdateNotification){
            print("正在滾動(dòng)..");
            // 當(dāng)前滾動(dòng)的位置和總長(zhǎng)度
            final currentPixel = notification.metrics.pixels;
            final totalPixel = notification.metrics.maxScrollExtent;
            double progress = currentPixel / totalPixel;
            setState(() {
              _isShowFloatButton = notification.metrics.pixels >= 1000;
              _progress = (progress * 100).toInt();
            });

            print("當(dāng)前滾動(dòng)位置:${notification.metrics.pixels}");
            print("總滾動(dòng)位置:${notification.metrics.maxScrollExtent}");
          }

          return true;
        },
        child: Stack(
          alignment: Alignment.center,
          children:[
            ListView.builder(
              controller: _controller,
              itemBuilder: (BuildContext ctx, int index) {
                return ListTile(
                  leading: Icon(Icons.pets),
                  title: Text("聯(lián)系人$index"),
                );
              },
              itemCount: 300,
            ),
            CircleAvatar(
              radius: 30,
              child: Text("$_progress%"),
              backgroundColor: Colors.black54,
            )
          ],
        ),
      ),
      floatingActionButton: _isShowFloatButton ? FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: () {
          //controller.jumpTo(0);
          _controller.animateTo(0, duration: Duration(milliseconds: 300), curve: Curves.easeIn);
        },
      ) : null,
      floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
      floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _controller.dispose();
  }
}
?著作權(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)容

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