CustomScrollView:一個滾動的容器,改組件不接受任何 child,可是你能夠直接提供 Slivers
已建立各類滾動效果,例如頁面中有多個可滑動的列表,如 Appbar, 列表,網格,等這種就能夠直接使用 SliverAppBar
,SliverList
和 SliverGrid
php
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))
],
),
);
}
}
複製代碼
運行效果以下:網絡
其實咱們仔細一點就會發現,其實 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
在上面的例子中 SliverList 使用的是 SliverChildBuilderDelegate
這個delegate,它能夠實現動態加載,固然 SliverList 中也有和 ListView 中同樣的非動態加載的delegate,就是SliverChildListDelegate
ide
SliverList(
delegate: SliverChildListDelegate(
[
FlutterLogo(size: 100),
FlutterLogo(size: 100),
FlutterLogo(size: 100),
],
))
複製代碼
通常在列表數量較小而且顯示內容肯定的狀況下可使用次 delegate
。性能
面的子元素中的寬高是動態的,須要手動設置高度,而且這種也不利於性能,因此咱們可使用 SliverFixedExtentList
來控制限制子元素的大小:
SliverFixedExtentList(
itemExtent: 100,
delegate: SliverChildListDelegate(
[
FlutterLogo(),
FlutterLogo(),
FlutterLogo(),
],
))
複製代碼
未限制前:,限制後:
通常狀況下,只要固定了列表中元素的高度,就能夠提高不小的性能,可是在實際的項目中,想要固定元素的高度是很是麻煩的,就算是列表中的元素只有一行文字,也有可能會出現問題,例如直接在系統層面修改字體的大小,這也會致使高度的固定致使渲染出來的效果不盡人意。可是有了 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 的大小被修改了,因此渲染出來的仍是有問題。
它也接受一個 delegate,支持動態的加載,只不過內部的子元素會佔滿整個屏幕
SliverFillViewport(
delegate: SliverChildListDelegate([
Container(color: Colors.red),
Container(color: Colors.yellow),
Container(color: Colors.blue),
]))
複製代碼
在 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,
],
),
),
複製代碼
透明組件,內部接受的是一個 sliver,因此須要用 SliverToAdapter 轉一下
SliverOpacity(
opacity: 0.5,
sliver: SliverToBoxAdapter(
child: FlutterLogo(
size: 100,
),
),
)
複製代碼
該組件會填滿當前頁面的剩餘空間
SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: CircularProgressIndicator(),
),
)
複製代碼
首先看一下實現的效果(因爲是 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站王叔不禿
若是本文有幫助到你的地方,不勝榮幸,若有文章中有錯誤和疑問,歡迎你們提出!