Day08 - Flutter -滾動(dòng)Widget

概述

  • ListView
  • GridView
  • sliver
  • 滾動(dòng)的監(jiān)聽(tīng)
一、ListView

移動(dòng)端數(shù)據(jù)量比較大時(shí),我們都是通過(guò)列表來(lái)進(jìn)行展示的,比如商品數(shù)據(jù)、聊天列表、通信錄、朋友圈等。
Android中,我們可以使用ListViewRecyclerView來(lái)實(shí)現(xiàn),在iOS中,我們可以通過(guò)UITableView來(lái)實(shí)現(xiàn)。
Flutter中,我們也有對(duì)應(yīng)的列表Widget,就是ListView

  • 1.1、ListView 基本創(chuàng)建
    ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來(lái)排列其所有子Widget。
    一種最簡(jiǎn)單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。
    我們來(lái)看一下直接使用ListView的代碼演練:

    • 1>、為了讓文字之間有一些間距,我使用了Padding Widget


      ListView的基本創(chuàng)建
      class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          return ListView(
             children: <Widget>[
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("人的一切痛苦,本質(zhì)上都是對(duì)自己無(wú)能的憤怒。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("人活在世界上,不可以有偏差;而且多少要費(fèi)點(diǎn)勁兒,才能把自己保持到理性的軌道上。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
                Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Text("我活在世上,無(wú)非想要明白些道理,遇見(jiàn)些有趣的事。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),),
                ),
              ],
            );
        }
      }
      

      提示:我們可以通過(guò) List.generate創(chuàng)建子 Widget

      • List.generate(100, (index):第一個(gè)參數(shù)是加載多少個(gè)Widget, 第二個(gè)是第幾個(gè)

        class MyHomeBody extends StatelessWidget {
          @override
          Widget build(BuildContext context) {
        
             return ListView(
                children: List.generate(100, (index) {
                   return Text("Hello World $index");
                })
             );
          }
        }
        
    • 2>、ListTile的使用
      在開(kāi)發(fā)中,我們經(jīng)常見(jiàn)到一種列表,有一個(gè)圖標(biāo)或圖片(Icon),有一個(gè)標(biāo)題(Title),有一個(gè)子標(biāo)題(Subtitle),還有尾部一個(gè)圖標(biāo)(Icon)。
      這個(gè)時(shí)候,我們可以使用ListTile來(lái)實(shí)現(xiàn):


      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
      
            return ListView(
                children: <Widget>[
                     ListTile(
                        leading: Icon(Icons.people, size: 20,),
                        title: Text("聯(lián)系人"),
                        subtitle: Text("聯(lián)系人信息"),
                        trailing: Icon(Icons.arrow_right),
                     ),
                     ListTile(
                        leading: Icon(Icons.people, size: 20,),
                        title: Text("郵箱"),
                        subtitle: Text("郵箱地址信息"),
                        trailing: Icon(Icons.arrow_right),
                     ),
                ],
            );
         }
      }
      
    • 3>、垂直方向滾動(dòng),默認(rèn)是垂直方向
      我們可以通過(guò)設(shè)置 scrollDirection 參數(shù)來(lái)控制視圖的滾動(dòng)方向
      我們通過(guò)下面的代碼實(shí)現(xiàn)一個(gè)水平滾動(dòng)的內(nèi)容:
      這里需要注意,我們需要給Container設(shè)置width,否則它是沒(méi)有寬度的,就不能正常顯示。或者我們也可以給ListView設(shè)置一個(gè) itemExtent該屬性會(huì)設(shè)置滾動(dòng)方向上每個(gè)item所占據(jù)的寬度

      class MyHomeBody extends StatelessWidget {
      
        @override
        Widget build(BuildContext context) {
            return ListView(
                scrollDirection: Axis.horizontal,
                itemExtent: 200,
                children: <Widget>[
                    Container(color: Colors.red, width: 200),
                    Container(color: Colors.green, width: 200),
                    Container(color: Colors.blue, width: 200),
                    Container(color: Colors.purple, width: 200),
                    Container(color: Colors.orange, width: 200),
                ],
            );
        }
      }
      
  • 1.2、ListView.build 創(chuàng)建
    通過(guò)構(gòu)造函數(shù)中的children傳入所有的子Widget有一個(gè)問(wèn)題:默認(rèn)會(huì)創(chuàng)建出所有的子Widget。
    但是對(duì)于用戶來(lái)說(shuō),一次性構(gòu)建出所有的Widget并不會(huì)有什么差異,但是對(duì)于我們的程序來(lái)說(shuō)會(huì)產(chǎn)生性能問(wèn)題,而且會(huì)增加首屏的渲染時(shí)間。
    我們可以ListView.build來(lái)構(gòu)建子Widget,提供性能。

    class MyHomeBody extends StatelessWidget {
    
         @override
         Widget build(BuildContext context) {
             return ListView.builder(
                // 創(chuàng)建多少個(gè) row
                itemCount: 50,
                // 滾動(dòng)方向的 row 寬度
                itemExtent: 100,
                // 生成 Widget
                itemBuilder: (BuildContext ctx, int index) {
                   return ListTile(title: Text("標(biāo)題$index"), subtitle: Text("詳情內(nèi)容$index"));
                }
             );
         }
    }
    
  • 1.3、ListView.separated 創(chuàng)建(帶分割線)
    ListView.separated 可以生成列表項(xiàng)之間的分割器,它除了比ListView.builder多了一個(gè)separatorBuilder參數(shù),該參數(shù)是一個(gè)分割器生成器。
    下面我們看一個(gè)例子:奇數(shù)行添加一條藍(lán)色下劃線,偶數(shù)行添加一條紅色下劃線:

    ListView.separated 創(chuàng)建(帶分割線)

    class MyHomeBody extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
            return ListView.separated(
                itemBuilder: (BuildContext context, int index) {
                   return ListTile(
                      leading: Icon(Icons.people),
                      trailing: Icon(Icons.arrow_right),
                      title: Text("聯(lián)系人${index+1}"),
                      subtitle: Text("聯(lián)系人電話${index+1}"),
                   );
                },
                itemCount: 10,
                separatorBuilder: (BuildContext context, int index) {
                   return Divider(
                     // 每個(gè)Widget 之間的距離
                     height: 30,
                     // 距離左邊的距離
                     indent: 16,
                     // 距離右邊的距離
                     endIndent: 16,
                     // 每條分割線的高度
                     thickness: 10,
                     color: index % 2 == 0 ? Colors.red : Colors.green,
                   );
                 },
             );
        }
    }
    
二、GridView 組件

GridView用于展示多列的展示,在開(kāi)發(fā)中也非常常見(jiàn),比如直播App中的主播列表、電商中的商品列表等等。
在Flutter中我們可以使用GridView來(lái)實(shí)現(xiàn),使用方式和ListView也比較相似。

  • 2.1、GridView構(gòu)造函數(shù)
    使用GridView的方式就是使用構(gòu)造函數(shù)來(lái)創(chuàng)建,和ListView對(duì)比有一個(gè)特殊的參數(shù):gridDelegate
    gridDelegate用于控制交叉軸的item數(shù)量或者寬度,需要傳入的類型是SliverGridDelegate,但是它是一個(gè)抽象類,所以我們需要傳入它的子類:

    • SliverGridDelegateWithFixedCrossAxisCount

      SliverGridDelegateWithFixedCrossAxisCount({
         @requireddouble crossAxisCount, // 交叉軸的item個(gè)數(shù)
         double mainAxisSpacing = 0.0,   // 主軸的間距
         double crossAxisSpacing = 0.0,  // 交叉軸的間距
         double childAspectRatio = 1.0,  // 子Widget的寬高比
      })
      

      如下代碼

      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
             return GridView(
                 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 3,
                    crossAxisSpacing: 20,
                    mainAxisSpacing: 20,
                    // 寬 / 高
                    childAspectRatio: 2
                 ),
                 children: List.generate(100, (index) {
                      return Container(
                          color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                      );
                 }
             ),
          );
        }
      }
      
    • SliverGridDelegateWithMaxCrossAxisExtent

      SliverGridDelegateWithMaxCrossAxisExtent({
         double maxCrossAxisExtent, // 交叉軸的item寬度
         double mainAxisSpacing = 0.0, // 主軸的間距
         double crossAxisSpacing = 0.0, // 交叉軸的間距
         double childAspectRatio = 1.0, // 子Widget的寬高比
      })
      

      如下代碼

      class MyHomeBody extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
              return GridView(
                  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent (
                      maxCrossAxisExtent: 100,
                      mainAxisSpacing: 20,
                      crossAxisSpacing: 20,
                      childAspectRatio: 2
                  ),
                  children: List.generate(100, (index) {
                      return Container(
                         color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                      );
                  }
              ),
           );
         }
      }
      

    提示:前面兩種方式也可以不設(shè)置delegate,可以分別使用:GridView.count構(gòu)造函數(shù)和GridView.extent構(gòu)造函數(shù)實(shí)現(xiàn)相同的效果

  • 2.2. GridView.build
    和ListView一樣,使用構(gòu)造函數(shù)會(huì)一次性創(chuàng)建所有的子Widget,會(huì)帶來(lái)性能問(wèn)題,所以我們可以使用GridView.build來(lái)交給GridView自己管理需要?jiǎng)?chuàng)建的子Widget。
    我們直接使用之前的數(shù)據(jù)來(lái)進(jìn)行代碼演練:


    GridView.build
    class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
          return Padding(
             padding: const EdgeInsets.all(5.0),
             child: GridView.builder(
                  shrinkWrap: true,
                  physics: ClampingScrollPhysics(),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount (
                      crossAxisCount: 2,
                      mainAxisSpacing: 10,
                      crossAxisSpacing: 10,
                      childAspectRatio: 1.2
                  ),
                  itemCount: 10,
                  itemBuilder: (BuildContext context, int index) {
                      return Container(
                           child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: <Widget>[
                                  Image.network('http://image.xcar.com.cn/attachments/a/day_200323/2020032314_59939b0716c40f9be872JrmcP75B4KfO.jpg-app'),
                                  SizedBox(height: 5),
                                  Text('王三', style: TextStyle(fontSize: 6),), 
                              ],
                           ),
                      );
                  }
              ),
         );
      }
    }
    
三、Sliver
  • 3.1、Sliver 的簡(jiǎn)單介紹
    我們考慮一個(gè)這樣的布局:一個(gè)滑動(dòng)的視圖中包括一個(gè)標(biāo)題視圖(HeaderView),一個(gè)列表視圖(ListView),一個(gè)網(wǎng)格視圖(GridView)。
    我們?cè)趺纯梢宰屗鼈冏龅浇y(tǒng)一的滑動(dòng)效果呢?使用前面的滾動(dòng)是很難做到的。
    Flutter中有一個(gè)可以完成這樣滾動(dòng)效果的Widget:CustomScrollView,可以統(tǒng)一管理多個(gè)滾動(dòng)視圖。
    在CustomScrollView中,每一個(gè)獨(dú)立的,可滾動(dòng)的Widget被稱之為Sliver。
    補(bǔ)充:Sliver可以翻譯成裂片、薄片,你可以將每一個(gè)獨(dú)立的滾動(dòng)視圖當(dāng)做一個(gè)小裂片。

  • 3.2、Slivers 的基本使用
    因?yàn)槲覀冃枰押芏嗟腟liver放在一個(gè)CustomScrollView中,所以CustomScrollView有一個(gè)slivers屬性,里面讓我們放對(duì)應(yīng)的一些Sliver:不可以放棄他的

    • SliverList:類似于我們之前使用過(guò)的ListView;

    • SliverFixedExtentList:類似于SliverList只是可以設(shè)置滾動(dòng)的高度;

    • SliverGrid:類似于我們之前使用過(guò)的GridView;

    • SliverPadding:設(shè)置Sliver的內(nèi)邊距,因?yàn)榭赡芤獑为?dú)給Sliver設(shè)置內(nèi)邊距;

    • SliverAppBar:添加一個(gè)AppBar,通常用來(lái)作為CustomScrollView的HeaderView;

    • SliverSafeArea:設(shè)置內(nèi)容顯示在安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容),也就是可以滾動(dòng)過(guò)安全區(qū)域

      class MyHomeBody1 extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
             return CustomScrollView(
                  slivers: <Widget>[
                      SliverGrid(
                           gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                               crossAxisCount: 2,
                               crossAxisSpacing: 16,
                               childAspectRatio: 2,
                               mainAxisSpacing: 16
                           ),
                           delegate: SliverChildBuilderDelegate(
                               (BuildContext context, int index) {
                                   return Container(
                                      color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                                   );
                               },
                               childCount: 10
                           )
                      ),
                  ],
            );
         }
      }
      
  • 3.3、Slivers的組合使用:SliverAppBar、SliverGrid、SliverList 的設(shè)置


    多個(gè)slivers的使用:SliverAppBar、SliverGrid、SliverList 的設(shè)置
    class MyHomeBody extends StatelessWidget {
       @override
       Widget build(BuildContext context) {
            return CustomScrollView(
                slivers: <Widget>[
                    SliverAppBar(
                       // true: bar不動(dòng)
                       // false: bar動(dòng)
                       pinned: true,
                       // bar 的高度
                      expandedHeight: 200,
                      flexibleSpace: FlexibleSpaceBar(
                          title: Text("Hello World!"),
                          background: Image.asset("assets/images/iron.png", fit: BoxFit.cover,),
                      ),
                   ),
                   SliverSafeArea(
                      sliver: SliverPadding(
                         padding: EdgeInsets.all(16),
                         sliver: SliverGrid(
                              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                              crossAxisCount: 2,
                              crossAxisSpacing: 16,
                              childAspectRatio: 2,
                              mainAxisSpacing: 16
                         ),
                         delegate: SliverChildBuilderDelegate(
                             (BuildContext context, int index) {
                               return Container(
                                  color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)),
                               );
                            },
                            childCount: 6
                         )
                      ),
                    ),
                  ),
                  SliverList(
                     delegate: SliverChildBuilderDelegate(
                         (BuildContext context, int index) {
                            return ListTile(
                               leading: Icon(Icons.people),
                               title: Text("聯(lián)系人"),
                            );
                         },
                         childCount: 20
                     ),
                  )
             ],
         );
      }
    }
    
四、滾動(dòng)的監(jiān)聽(tīng)

對(duì)于滾動(dòng)的視圖,我們經(jīng)常需要監(jiān)聽(tīng)它的一些滾動(dòng)事件,在監(jiān)聽(tīng)到的時(shí)候去做對(duì)應(yīng)的一些事情。
比如視圖滾動(dòng)到底部時(shí),我們可能希望做上拉加載更多;
比如滾動(dòng)到一定位置時(shí)顯示一個(gè)回到頂部的按鈕,點(diǎn)擊回到頂部的按鈕,回到頂部;
比如監(jiān)聽(tīng)滾動(dòng)什么時(shí)候開(kāi)始,什么時(shí)候結(jié)束;
Flutter 中監(jiān)聽(tīng)滾動(dòng)相關(guān)的內(nèi)容由兩部分組成ScrollControllerScrollNotification

  • 4.1、ScrollController 監(jiān)聽(tīng),可以預(yù)先設(shè)置offset,也可以監(jiān)聽(tīng)滾動(dòng)的位置,缺點(diǎn)是:無(wú)法檢測(cè)股東開(kāi)始和結(jié)束
    在Flutter中,Widget并不是最終渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常這種監(jiān)聽(tīng)事件以及相關(guān)的信息并不能直接從Widget中獲取,而是必須通過(guò)對(duì)應(yīng)的Widget的Controller來(lái)實(shí)現(xiàn)。
    ListView、GridView的組件控制器是ScrollController,我們可以通過(guò)它來(lái)獲取視圖的滾動(dòng)信息,并且可以調(diào)用里面的方法來(lái)更新視圖的滾動(dòng)位置。
    另外,通常情況下,我們會(huì)根據(jù)滾動(dòng)的位置來(lái)改變一些Widget的狀態(tài)信息,所以ScrollController通常會(huì)和StatefulWidget一起來(lái)使用,并且會(huì)在其中控制它的初始化、監(jiān)聽(tīng)、銷毀等事件。
    我們來(lái)做一個(gè)案例,當(dāng)滾動(dòng)到500位置的時(shí)候,顯示一個(gè)回到頂部的按鈕:

    • jumpTo(double offset)、animateTo(double offset,...):這兩個(gè)方法用于跳轉(zhuǎn)到指定的位置,它們不同之處在于,后者在跳轉(zhuǎn)時(shí)會(huì)執(zhí)行一個(gè)動(dòng)畫(huà),而前者不會(huì)。

    • ScrollController間接繼承自Listenable,我們可以根據(jù)ScrollController來(lái)監(jiān)聽(tīng)滾動(dòng)事件。



      代碼如下

      class HomePage extends StatefulWidget {
         @override
         _HomePageState createState() => _HomePageState();
      }
      
      class _HomePageState extends State<HomePage> {
         // 設(shè)置變量 _controller 并設(shè)置偏移量
         ScrollController _controller = ScrollController(initialScrollOffset: 200);
         /* 默認(rèn)設(shè)置為 false */
         bool _isFloatingActionButton = false;
         @override
         void initState() {
             // TODO: implement initState
             super.initState();
      
             _controller.addListener(() {
                print("監(jiān)聽(tīng)到滾動(dòng)");
                setState(() {
                    _isFloatingActionButton = _controller.offset > 500 ? true : false;
                });
            });
         }
      
         @override
         Widget build(BuildContext context) {
            return Scaffold(
               appBar: AppBar(
                   title: Text("列表滾動(dòng)測(cè)試"),
               ),
               body: ListView.builder(
                   controller: _controller,
                   itemCount: 20,
                   itemBuilder: (BuildContext context, int index) {
                     return ListTile(
                        leading: Icon(Icons.people),
                        title: Text("測(cè)試 $index"),
                     );
                   }
               ),
               floatingActionButton: _isFloatingActionButton ? FloatingActionButton(
                  child: Icon(Icons.arrow_upward),
                  onPressed: () {
                    // 返回到頂部
                    _controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn);
                  },
               ) : null,
           );
      
         }
      }
      
  • 4.2、ScrollNotification
    如果我們希望監(jiān)聽(tīng)什么時(shí)候開(kāi)始滾動(dòng),什么時(shí)候結(jié)束滾動(dòng),這個(gè)時(shí)候我們可以通過(guò)NotificationListener。

    • NotificationListener是一個(gè)Widget,模板參數(shù)T是想監(jiān)聽(tīng)的通知類型,如果省略,則所有類型通知都會(huì)被監(jiān)聽(tīng),如果指定特定類型,則只有該類型的通知會(huì)被監(jiān)聽(tīng)。
    • NotificationListener需要一個(gè)onNotification回調(diào)函數(shù),用于實(shí)現(xiàn)監(jiān)聽(tīng)處理邏輯。

    該回調(diào)可以返回一個(gè)布爾值,代表是 false 阻止該事件繼續(xù)向上冒泡,如果為true時(shí),則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時(shí),則冒泡繼續(xù)。
    案例: 列表滾動(dòng), 并且在中間顯示滾動(dòng)進(jìn)度

    class HomePage extends StatefulWidget {
       @override
        _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
        int _progress = 0;
    
        @override
        Widget build(BuildContext context) {
             return Scaffold(
                  appBar: AppBar(
                     title: Text("列表滾動(dòng)測(cè)試"),
                  ),
                  body: NotificationListener(
                     onNotification: (ScrollNotification notification ) {
                          if (notification is ScrollStartNotification) {
                             print("----開(kāi)始滾動(dòng)----");
                          } else if (notification is ScrollUpdateNotification) {
                               // 當(dāng)前滾動(dòng)的位置和總長(zhǎng)度
                               final currentPixel = notification.metrics.pixels;
                               final totalPixel = notification.metrics.maxScrollExtent;
                               double progress = currentPixel / totalPixel;
                               setState(() {
                                  _progress = (progress * 100).toInt();
                               });
                               print("正在滾動(dòng):${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
                          } else if (notification is ScrollEndNotification) {
                               print("----結(jié)束滾動(dòng)----");
                          }
                          return true;
                     },
                     child: Stack(
                          alignment: Alignment(0.9, 0.9),
                          children: <Widget>[
                              ListView.builder(
                                   itemCount: 100,
                                   itemExtent: 60,
                                   itemBuilder: (BuildContext context, int index) {
                                        return ListTile(title: Text("item$index"));
                                   }
                              ),
                              CircleAvatar(
                                   radius: 30,
                                   child: Text("$_progress%"),
                                   backgroundColor: Colors.black54,
                              )
                          ],
                     ),
               ),
           );
        }
     }
    
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。