Flutter | Slivers 系列

概述

CustomScrollView:一個滾動的容器,改組件不接受任何 child,可是你能夠直接提供 Slivers 已建立各類滾動效果,例如頁面中有多個可滑動的列表,如 Appbar, 列表,網格,等這種就能夠直接使用 SliverAppBar,SliverListSliverGridphp

Slivers 不是單獨指一個組件,而是指的一個系列,因此以 Sliver 開頭的組件都是這個系列的,可是他們都只能做用於 CustomScrollView 中。web

經常使用到的 Sliver 有,SliverAppbar,SliverList,SliverGrid,SliverToBoxAdapter 等json

因爲 CustomScrollView 的子組件只能是 Sliver 系列,若是要將一個普通的組件放在裏面,必須使用 SliverToBoxAdapter 進行適配才行markdown

簡單的使用

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      drawer: Drawer(),
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            title: Text("SliverAppbar"),
          ),
          SliverGrid(
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
            delegate: SliverChildBuilderDelegate((context, index) {
              return Container(
                  color: Colors.primaries[index % Colors.primaries.length]);
            }, childCount: 40),
          ),
          SliverList(
              delegate: SliverChildBuilderDelegate((context, index) {
            return Container(
                height: 100,
                color: Colors.primaries[index % Colors.primaries.length]);
          }, childCount: 20))
        ],
      ),
    );
  }
}
複製代碼

運行效果以下:網絡

345

其實咱們仔細一點就會發現,其實 ListView 和 GridView 等組件內部使用的都是 Slivers,app

ListView.builder({
 //......
}) : assert(itemCount == null || itemCount >= 0),
     assert(semanticChildCount == null || semanticChildCount <= itemCount!),
     childrenDelegate = SliverChildBuilderDelegate(
       itemBuilder,
       childCount: itemCount,
       addAutomaticKeepAlives: addAutomaticKeepAlives,
       addRepaintBoundaries: addRepaintBoundaries,
       addSemanticIndexes: addSemanticIndexes,
     ),
     super(
 		//....
     );
複製代碼

那爲何要使用 Slivers 呢?最主要的緣由就是能夠在 slives 中添加多個組件,如在列表的上面和下面添加更多的內容。less

而且 slivers 中,若是存在多個列表的話也是支持動態加載的,而不是會一次性所有渲染完async


各式各樣的 Slivers 組件

SliverList

在上面的例子中 SliverList 使用的是 SliverChildBuilderDelegate 這個delegate,它能夠實現動態加載,固然 SliverList 中也有和 ListView 中同樣的非動態加載的delegate,就是SliverChildListDelegateide

SliverList(
    delegate: SliverChildListDelegate(
  [
    FlutterLogo(size: 100),
    FlutterLogo(size: 100),
    FlutterLogo(size: 100),
  ],
))
複製代碼

通常在列表數量較小而且顯示內容肯定的狀況下可使用次 delegate性能

SliverFixedExtentList

面的子元素中的寬高是動態的,須要手動設置高度,而且這種也不利於性能,因此咱們可使用 SliverFixedExtentList 來控制限制子元素的大小:

SliverFixedExtentList(
    itemExtent: 100,
    delegate: SliverChildListDelegate(
      [
        FlutterLogo(),
        FlutterLogo(),
        FlutterLogo(),
      ],
    ))
複製代碼

未限制前:image-20210810143917248,限制後:image-20210810144017236

SliverPrototypeExtentList

通常狀況下,只要固定了列表中元素的高度,就能夠提高不小的性能,可是在實際的項目中,想要固定元素的高度是很是麻煩的,就算是列表中的元素只有一行文字,也有可能會出現問題,例如直接在系統層面修改字體的大小,這也會致使高度的固定致使渲染出來的效果不盡人意。可是有了 SliverPrototypeExtentList 就簡單多了。

在 SliverPrototypeExtentList 中,能夠經過 prototypeItem 來傳入一個原型,這個原型並不會渲染到屏幕上,在運行的過程當中,Flutter 會將原型的尺寸計算出來,以後就會把全部的元素尺寸設置成這個原型的尺寸。

body: DefaultTextStyle(
  style: TextStyle(fontSize: 60, color: Colors.red),
  child: CustomScrollView(
    slivers: [
      SliverPrototypeExtentList(
          prototypeItem: Text(""),
          delegate: SliverChildListDelegate(
            [
              Text("Hello Word"),
              Text("Hello Word"),
              Text("Hello Word"),
            ],
          )),
    ],
  ),
),
複製代碼

如上,子元素的大小都會和 prototypeItem 中元素的大小進行同步,咱們和 SliverFixedExtentList 對比看一下效果

body: DefaultTextStyle(
  style: TextStyle(fontSize: 60, color: Colors.red),
  child: CustomScrollView(
    slivers: [
      SliverFixedExtentList(
          itemExtent: 40,
          delegate: SliverChildListDelegate(
            [
              Text("Hello Word"),
              Text("Hello Word"),
              Text("Hello Word"),
            ],
          )),
    ],
  ),
),
複製代碼

效果以下:

使用 prototype:image-20210810150249268,使用 fixed:image-20210810150332297

從圖中能夠看到,儘管高度固定到 40,可是因爲 Text 的大小被修改了,因此渲染出來的仍是有問題。


SliverFillViewport

它也接受一個 delegate,支持動態的加載,只不過內部的子元素會佔滿整個屏幕

SliverFillViewport(
    delegate: SliverChildListDelegate([
  Container(color: Colors.red),
  Container(color: Colors.yellow),
  Container(color: Colors.blue),
]))
複製代碼
345

SliverAppbar

在 slivers 系列中,SliverAppbar 能夠說是使用頻率比較高的組件了,SliverAppbar 爲應用欄提供了自定義滾動行爲,下面咱們來看一下

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: Drawer(),
      body: DefaultTextStyle(
        style: TextStyle(fontSize: 60, color: Colors.red),
        child: CustomScrollView(
          slivers: [
            SliverAppBar(
              title: Text("Sliver AppBar"),
            ),
            SliverToBoxAdapter(child: Placeholder()),
            SliverList(
              delegate: SliverChildListDelegate(
                [
                  FlutterLogo(size: 200),
                  FlutterLogo(size: 200),
                  FlutterLogo(size: 200),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
複製代碼

上面是一個磨人的 SliverAppbar,並無實現任何特殊效果,默認的效果以下:

345

能夠看到在滑動的過程當中,SliverAppbar 被頂上去了,這也是很是正常的。接着咱們來看一下都有哪些特殊效果吧

特殊效果
  • floating

    SliverAppBar(
      title: Text("Sliver AppBar"),
      floating: true,
    )
    複製代碼

    在向下滑動的時候,會首先將 SliveAppbar 顯示出來,以下:

    345
  • pinned :一直顯示在頂部,無視滑動,這樣就和普通的導航欄差很少了。區別就是在滑動的時候 SliveAppbar 的底部會有一點點影子

  • snap:在滑動中止以後,導航會自動所有顯示出來,須要注意的是必須搭配 floating 一塊兒使用,以下:

    SliverAppBar(
      title: Text("Sliver AppBar"),
      snap: true,
      floating: true,
    )
    複製代碼
    345
  • flexibleSpace:可展開拉伸的部分

    SliverAppBar(
      // title: Text("Sliver AppBar"),
      expandedHeight: 300,
      stretch: true,
      flexibleSpace: FlexibleSpaceBar(
        background: FlutterLogo(),
        title: Text("FlexibleSpaceBar title"),
        collapseMode: CollapseMode.parallax,
        stretchModes: [
          StretchMode.blurBackground,
          StretchMode.zoomBackground,
          StretchMode.fadeTitle,
        ],
      ),
    ),
    複製代碼
345

SliverOpacity

透明組件,內部接受的是一個 sliver,因此須要用 SliverToAdapter 轉一下

SliverOpacity(
  opacity: 0.5,
  sliver: SliverToBoxAdapter(
    child: FlutterLogo(
      size: 100,
    ),
  ),
)
複製代碼

SliverFillRemaining

該組件會填滿當前頁面的剩餘空間

SliverFillRemaining(
  hasScrollBody: false,
  child: Center(
    child: CircularProgressIndicator(),
  ),
)
複製代碼
  • hasScrollBody :當前組件中是否有可滾動的組件
image-20210810174859170

案例

首先看一下實現的效果(因爲是 gif 圖,因此看起來有一點卡):

345
  • 準備數據

    接口來源於網絡,僅供學習使用

    https://h5.48.cn/resource/jsonp/allmembers.php?gid=10
    複製代碼

    對應的數據類:

    class Member {
      final String id;
      final String name;
      final String team;
      final String sid;
      final String gid;
      final String gname;
      final String sname;
      final String fname;
      final String tname;
      final String pid;
      final String pname;
      final String nickname;
      final String company;
      final String join_day;
      final String height;
      final String birth_day;
      final String star_sign_12;
      final String star_sign_48;
      final String speciality;
      final String hobby;
      final String experience;
      final String catch_phrase;
      final String status;
      final String ranking;
      final String tcolor;
      final String gcolor;
    
      String get avatarUrl => "https://www.snh48.com/images/member/zp_$id.jpg";
    
      Member(
        this.id,
        this.name,
       //.....自行添加
      );
    
      @override
      String toString() {
        return "$id --- $name";
      }
    }
    複製代碼
  • 首頁

    class _DemoWidgetState extends State<DemoWidget> {
      List<Member> _member = [];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("案例"),
          ),
          body: RefreshIndicator(
            onRefresh: () async {
              setState(() => _member.clear());
              final url = "https://h5.48.cn/resource/jsonp/allmembers.php?gid=10";
              final res = await http.get(Uri.parse(url));
              if (res.statusCode != 200) throw Error();
    
              final json = convert.jsonDecode(res.body);
              final members = (json["rows"] as List)
                  .map((e) => Member(
                        e['sid'], e["sname"],e["tname"], e["sid"], e["gid"],e["gname"],e["sname"],e["fname"],e["tname"],
                        e["pid"],e["pname"], e["nickname"], e["company"], e["join_day"], e["height"],    e["birth_day"],
                        e["star_sign_12"], e["star_sign_48"], e["speciality"], e["hobby"], e["experience"],
                        e["catch_phrase"],  e["status"], e["ranking"], e["tcolor"],e["gcolor"],
                      ))
                  .toList();
    
              setState(() => _member = members);
            },
            child: CustomScrollView(
              slivers: [
                SliverToBoxAdapter(),
                SliverPersistentHeader(
                    delegate: _MyDelegate("SII", Color(0xffae86bb)), pinned: true),
                _buildTeamList("SII"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("NII", Color(0xff91cdeb)), pinned: true),
                _buildTeamList("NII"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("HII", Color(0xffa7b0ba)), pinned: true),
                _buildTeamList("HII"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("預備生", Color(0xff91cdeb)), pinned: true),
                _buildTeamList("預備生"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("榮譽畢業生", Color(0xff8ed2f5)),
                    pinned: true),
                _buildTeamList("榮譽畢業生"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("S預備生", Color(0xff38b26d)), pinned: true),
                _buildTeamList("S預備生"),
                SliverPersistentHeader(
                    delegate: _MyDelegate("X", Color(0xffa7b0ba)), pinned: true),
                _buildTeamList("X"),
              ],
            ),
          ),
        );
      }
    
      SliverGrid _buildTeamList(String teamName) {
        //進行篩選
        final teamMember =
            _member.where((element) => element.team == teamName).toList();
        return SliverGrid(
          delegate: SliverChildBuilderDelegate((context, index) {
            Member m = teamMember[index];
            return InkWell(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  //動畫
                  Hero(
                      tag: m.avatarUrl,
                      child: ClipOval(
                        child: CircleAvatar(
                          child: Image.network(m.avatarUrl),
                          backgroundColor: Colors.white,
                        ),
                      )),
                  Text("${m.name}"),
                ],
              ),
              onTap: () => Navigator.of(context)
                  .push(MaterialPageRoute(builder: (_) => DetailPage(m))),
            );
          }, childCount: teamMember.length),
          gridDelegate:
              SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 120),
        );
      }
    }
    
    
    class _MyDelegate extends SliverPersistentHeaderDelegate {
      final String title;
      final Color color;
    
      _MyDelegate(this.title, this.color);
    
      @override
      Widget build(
          BuildContext context, double shrinkOffset, bool overlapsContent) {
        return Container(
          height: 35,
          child: FittedBox(child: Text(title, style: TextStyle())),
          color: color,
        );
      }
    
      ///最高高度
      @override
      double get maxExtent => 35;
    
      ///最新高度
      @override
      double get minExtent => 35;
    
      ///重繪
      @override
      bool shouldRebuild(covariant _MyDelegate oldDelegate) {
        //若是 title 不相等,則重繪
        return oldDelegate.title != title;
      }
    }
    
    複製代碼

    上面代碼在 refresh 中進行了網絡請求,而後進行解析數據,最後進行了刷新操做

    上面代碼都很簡單,不太熟悉的可能就是 SliverPersistentHeader 了,這是一個能夠置頂的 header,它能夠出如今視圖的任何一個位置, pinnedfloating 屬性用來控制收起是是否展現,具體意思和 SliverAppbar 中同樣。

  • 詳情頁面

    class DetailPage extends StatelessWidget {
      final Member member;
    
      DetailPage(this.member);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            body: CustomScrollView(
          slivers: [
            SliverAppBar(
                expandedHeight: 300,
                pinned: true,
                stretch: true,
                flexibleSpace: FlexibleSpaceBar(
                  centerTitle: true,
                  title: Text("${member.name}"),
                  background: Center(
                    child: Padding(
                      padding: const EdgeInsets.all(100),
                      //長寬比
                      child: AspectRatio(
                        aspectRatio: 1,
                         // 和上面那個頁面的動畫對應,tag 必須一致
                        child: Hero(
                          tag: member.avatarUrl,
                          child: Material(
                            elevation: 4.0,
                            shape: CircleBorder(),
                            child: ClipOval(
                              child: Image.network(
                                member.avatarUrl,
                                fit: BoxFit.cover,
                              ),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                )),
            SliverList(
                delegate: SliverChildListDelegate(
              [
                _buildInfo("戰隊:", member.team),
                _buildInfo("公司:", member.company),
                _buildInfo("時間:", member.join_day),
                _buildInfo("身高:", member.height),
                _buildInfo("生日:", member.birth_day),
                _buildInfo("星座:", member.star_sign_12),
                _buildInfo("運勢:", member.star_sign_48),
                _buildInfo("愛好:", member.speciality),
                _buildInfo("簽名:", member.catch_phrase),
              ],
            ))
          ],
        ));
      }
    
      _buildInfo(String label, String content) {
        return Card(
          child: Padding(
            padding: EdgeInsets.symmetric(vertical: 25),
            child: Row(
              children: [Text(label), Text(content)],
            ),
          ),
        );
      }
    }
    複製代碼

    上面代碼中有一個問題,原本使用了 stretch 屬性以後,在下拉的時候應該會有一個放大的效果,可是運行代碼的時候並無,有知道緣由的同窗能夠講一下


參考:B站王叔不禿

若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!

相關文章
相關標籤/搜索