概述
CustomScrollView:一個滾動的容器,改元件不接受任何 child,但是你可以直接提供 Slivers
已建立各種滾動效果,例如頁面中有多個可滑動的列表,如 Appbar, 列表,網格,等這種就可以直接使用 SliverAppBar
,SliverList
和 SliverGrid
Slivers 不是單獨指一個元件,而是指的一個系列,所以以 Sliver 開頭的元件都是這個系列的,但是他們都只能作用於 CustomScrollView
中。
常用到的 Sliver 有,SliverAppbar,SliverList,SliverGrid,SliverToBoxAdapter 等
由於 CustomScrollView 的子元件只能是 Sliver 系列,如果要將一個普通的元件放在裡面,必須使用
SliverToBoxAdapter
進行適配才行
簡單的使用
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))
],
),
);
}
}
複製程式碼
執行效果如下:
其實我們仔細一點就會發現,其實 ListView 和 GridView 等元件內部使用的都是 Slivers,
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 中新增多個元件,如在列表的上面和下面新增更多的內容。
並且 slivers 中,如果存在多個列表的話也是支援動態載入的,而不是會一次性全部渲染完
各式各樣的 Slivers 元件
SliverList
在上面的例子中 SliverList 使用的是 SliverChildBuilderDelegate
這個delegate,它可以實現動態載入,當然 SliverList 中也有和 ListView 中一樣的非動態載入的delegate,就是SliverChildListDelegate
SliverList(
delegate: SliverChildListDelegate(
[
FlutterLogo(size: 100),
FlutterLogo(size: 100),
FlutterLogo(size: 100),
],
))
複製程式碼
一般在列表數量較小並且顯示內容確定的情況下可以使用次 delegate
。
SliverFixedExtentList
面的子元素中的寬高是動態的,需要手動設定高度,並且這種也不利於效能,所以我們可以使用 SliverFixedExtentList
來控制限制子元素的大小:
SliverFixedExtentList(
itemExtent: 100,
delegate: SliverChildListDelegate(
[
FlutterLogo(),
FlutterLogo(),
FlutterLogo(),
],
))
複製程式碼
未限制前:,限制後:
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:,使用 fixed:
從圖中可以看到,儘管高度固定到 40,但是由於 Text 的大小被修改了,所以渲染出來的還是有問題。
SliverFillViewport
它也接受一個 delegate,支援動態的載入,只不過內部的子元素會佔滿整個螢幕
SliverFillViewport(
delegate: SliverChildListDelegate([
Container(color: Colors.red),
Container(color: Colors.yellow),
Container(color: Colors.blue),
]))
複製程式碼
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,並沒有實現任何特殊效果,預設的效果如下:
可以看到在滑動的過程中,SliverAppbar 被頂上去了,這也是非常正常的。接著我們來看一下都有哪些特殊效果吧
特殊效果
-
floating
SliverAppBar( title: Text("Sliver AppBar"), floating: true, ) 複製程式碼
在向下滑動的時候,會首先將 SliveAppbar 顯示出來,如下:
-
pinned :一直顯示在頂部,無視滑動,這樣就和普通的導航欄差不多了。區別就是在滑動的時候 SliveAppbar 的底部會有一點點影子
-
snap:在滑動停止之後,導航會自動全部顯示出來,需要注意的是必須搭配 floating 一起使用,如下:
SliverAppBar( title: Text("Sliver AppBar"), snap: true, floating: true, ) 複製程式碼
-
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, ], ), ), 複製程式碼
SliverOpacity
透明元件,內部接受的是一個 sliver,所以需要用 SliverToAdapter 轉一下
SliverOpacity(
opacity: 0.5,
sliver: SliverToBoxAdapter(
child: FlutterLogo(
size: 100,
),
),
)
複製程式碼
SliverFillRemaining
該元件會填滿當前頁面的剩餘空間
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: CircularProgressIndicator(),
),
)
複製程式碼
- hasScrollBody :當前元件中是否有可滾動的元件
案例
首先看一下實現的效果(由於是 gif 圖,所以看起來有一點卡):
-
準備資料
介面來源於網路,僅供學習使用
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,它可以出現在檢視的任何一個位置,pinned
和floating
屬性用來控制收起是是否展示,具體意思和 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站王叔不禿
如果本文有幫助到你的地方,不勝榮幸,如有文章中有錯誤和疑問,歡迎大家提出!