目錄
1. PageView 分頁
2. TabBarView(對(duì)PageView進(jìn)行了封裝)
3. CustomScrollView
4. NestedScrollView
1. PageView 分頁
可用來實(shí)現(xiàn)頁面切換族壳、Tab換頁、視頻上下滑頁趣些、圖片輪動(dòng)仿荆。
PageView({
Key? key,
this.scrollDirection = Axis.horizontal, // 滾動(dòng)方向
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true, // true:滑動(dòng)超過一半則自動(dòng)切換否則還原。false:不切換
this.onPageChanged, // 頁面發(fā)生變化后回調(diào)
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
})
PageView.builder({
其他參數(shù)同上(沒有children參數(shù))
required IndexedWidgetBuilder itemBuilder,
int? itemCount,
})
最終都會(huì)生成一個(gè)SliverChildDelegate來負(fù)責(zé)列表項(xiàng)的按需加載,而在SliverChildDelegate中每當(dāng)列表項(xiàng)構(gòu)建完成后都會(huì)為其添加一個(gè)AutomaticKeepAlive父組件拢操。
示例
// Tab 頁面
class Page extends StatefulWidget {
const Page({
Key? key,
required this.text
}) : super(key: key);
final String text;
@override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
@override
Widget build(BuildContext context) {
print("build ${widget.text}");
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
}
創(chuàng)建一個(gè) PageView
@override
Widget build(BuildContext context) {
var children = <Widget>[];
// 生成 6 個(gè) Tab 頁
for (int i = 0; i < 6; ++i) {
children.add( Page( text: '$i'));
}
return PageView(
// scrollDirection: Axis.vertical, // 滑動(dòng)方向?yàn)榇怪狈较? children: children,
);
}
示例2
PageView(
onPageChanged: (currentPage)=>print('pageIndex:$currentPage'),
controller: PageController(
initialPage: 1, // 初始頁面
keepPage: false, //
viewportFraction: 0.85, // 頁面占用可視比例
),
children: [
Container(
color: Colors.brown[900],
alignment: Alignment(0,0),
child: Text(
'One',
style: TextStyle(
fontSize: 32,color: Colors.white,
),
),
),
Container(
color: Colors.grey[900],
alignment: Alignment(0,0),
child: Text(
'Two',
style: TextStyle(
fontSize: 32,color: Colors.white,
),
),
),
Container(
color: Colors.blueGrey[900],
alignment: Alignment(0,0),
child: Text(
'Three',
style: TextStyle(
fontSize: 32,color: Colors.white,
),
),
)
],
);
示例3(PageView.builder)
PageView.builder(
itemCount: posts.length,
itemBuilder: _pageItemBuilder,
)
//
Widget _pageItemBuilder(BuildContext context,int index){
return Stack(
children: [
SizedBox(
child: Image.network(
posts[index].imgUrl,
fit: BoxFit.cover,
),
),
Positioned(
bottom: 8.0,
left: 8.0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
posts[index].title,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(
posts[index].author,
style: TextStyle(
fontWeight: FontWeight.bold,
),
)
],
),
)
],
);
}
/*
頁面緩存
PageView 默認(rèn)并沒有緩存功能锦亦,一旦頁面滑出屏幕它就會(huì)被銷毀,這和我們前面講過的 ListView/GridView 不一樣令境,在創(chuàng)建 ListView/GridView 時(shí)我們可以手動(dòng)指定 ViewPort 之外多大范圍內(nèi)的組件需要預(yù)渲染和緩存(通過 cacheExtent 指定)杠园,只有當(dāng)組件滑出屏幕后又滑出預(yù)渲染區(qū)域,組件才會(huì)被銷毀舔庶,但是不幸的是 PageView 并沒有 cacheExtent 參數(shù)抛蚁。
PageView 創(chuàng)建Viewport 的代碼中是這樣的:
child: Scrollable(
...
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
...
);
},
)
發(fā)現(xiàn) 雖然 PageView 沒有透?jìng)?cacheExtent,但是卻在allowImplicitScrolling 為 true 時(shí)設(shè)置了預(yù)渲染區(qū)域惕橙,注意瞧甩,此時(shí)的緩存類型為 CacheExtentStyle.viewport,則 cacheExtent 則表示緩存的長度是幾個(gè) Viewport 的寬度弥鹦,cacheExtent 為 1.0肚逸,則代表前后各緩存一個(gè)頁面寬度,即前后各一頁惶凝。將 PageView 的 allowImplicitScrolling 置為 true 則不就可以緩存前后兩頁吼虎。
根據(jù)文檔以及注釋中 issue 的鏈接,發(fā)現(xiàn)PageView 中設(shè)置 cacheExtent 會(huì)和 iOS 中 輔助功能有沖突(讀者可以先不用關(guān)注)苍鲜,所以暫時(shí)還沒有什么好的辦法思灰。看到這可能國內(nèi)的很多開發(fā)者要說我們的 App 不用考慮輔助功能混滔,既然如此洒疚,那問題很好解決,將 PageView 的源碼拷貝一份坯屿,然后透?jìng)?cacheExtent 即可油湖。
*/
Flutter提供了一個(gè)AutomaticKeepAliveClientMixin ,只需要讓 PageState 混入這個(gè) mixin领跛,且同時(shí)添加一些必要操作:
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin{
@override
Widget build(BuildContext context) {
super.build(context); // 必須調(diào)用乏德。方法實(shí)現(xiàn)在 AutomaticKeepAliveClientMixin 中。根據(jù)當(dāng)前 wantKeepAlive 的值給 AutomaticKeepAlive 發(fā)送消息吠昭,AutomaticKeepAlive 收到消息后就會(huì)開始工作喊括。
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要緩存
}
需要注意,如果采用 PageView.custom 構(gòu)建頁面時(shí)沒有給列表項(xiàng)包裝 AutomaticKeepAlive 父組件矢棚,則上述方案不能正常工作郑什。
通過混入的方式實(shí)現(xiàn)緩存不是很優(yōu)雅,因?yàn)楸仨毟?Page 的代碼蒲肋,有侵入性蘑拯。
網(wǎng)上開發(fā)者自定義了一個(gè)KeepAliveWrapper組件钝满,只需要使用 KeepAliveWrapper包裹需要緩存的組件就可以了。
// KeepAliveWrapper(child:Page( text: '$i')
class KeepAliveWrapper extends StatefulWidget {
const KeepAliveWrapper({
Key? key,
this.keepAlive = true,
required this.child,
}) : super(key: key);
final bool keepAlive;
final Widget child;
@override
_KeepAliveWrapperState createState() => _KeepAliveWrapperState();
}
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
if(oldWidget.keepAlive != widget.keepAlive) {
// keepAlive 狀態(tài)需要更新申窘,實(shí)現(xiàn)在 AutomaticKeepAliveClientMixin 中
updateKeepAlive();
}
super.didUpdateWidget(oldWidget);
}
@override
bool get wantKeepAlive => widget.keepAlive;
}
示例
lass KeepAliveTest extends StatelessWidget {
const KeepAliveTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: (_, index) {
return KeepAliveWrapper(
// 為 true 后會(huì)緩存所有的列表項(xiàng)弯蚜,列表項(xiàng)將不會(huì)銷毀。
// 為 false 時(shí)偶洋,列表項(xiàng)滑出預(yù)加載區(qū)域后將會(huì)別銷毀熟吏。
// 使用時(shí)一定要注意是否必要,因?yàn)閷?duì)所有列表項(xiàng)都緩存的會(huì)導(dǎo)致更多的內(nèi)存消耗
keepAlive: true,
child: ListItem(index: index),
);
});
}
}
class ListItem extends StatefulWidget {
const ListItem({Key? key, required this.index}) : super(key: key);
final int index;
@override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(title: Text('${widget.index}'));
}
@override
void dispose() {
print('dispose ${widget.index}');
super.dispose();
}
}
2. TabBarView(對(duì)PageView進(jìn)行了封裝)
TabBarView({
Key? key,
required this.children, // tab 頁
this.controller, // TabController玄窝。用于監(jiān)聽和控制 TabBarView 的頁面切換牵寺,通常和 TabBar 聯(lián)動(dòng)。如果沒有指定恩脂,則會(huì)在組件樹中向上查找并使用最近的一個(gè)DefaultTabController帽氓。
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
})
// TabBar 為 TabBarView 的導(dǎo)航標(biāo)題,通常位于底部俩块。
TabBar({
Key? key,
required this.tabs, // Tabs
this.controller, // 如果需要和 TabBarView 聯(lián)動(dòng)黎休, TabBar 和 TabBarView 使用同一個(gè) TabController。注意玉凯,聯(lián)動(dòng)時(shí) TabBar 和 TabBarView 的孩子數(shù)量需要一致势腮。
this.isScrollable = false, // 是否可以滑動(dòng)
this.padding,
this.indicatorColor,// 指示器顏色,默認(rèn)是高度為2的一條下劃線
this.automaticIndicatorColorAdjustment = true,
this.indicatorWeight = 2.0,// 指示器高度
this.indicatorPadding = EdgeInsets.zero, //指示器padding
this.indicator, // 指示器
this.indicatorSize, // 指示器長度漫仆,有兩個(gè)可選值捎拯,一個(gè)tab的長度,一個(gè)是label長度
this.labelColor,
this.labelStyle,
this.labelPadding,
this.unselectedLabelColor,
this.unselectedLabelStyle,
this.mouseCursor,
this.onTap,
...
})
// tab可以是任何 Widget盲厌,但一般會(huì)使用Tab
Tab({
Key? key,
this.text, // 文本署照。text 和 child 是互斥的,不能同時(shí)指定吗浩。
this.icon, // 圖標(biāo)
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定義 widget
})
示例
// 為了實(shí)現(xiàn) TabBar 和 TabBarView 的聯(lián)動(dòng)建芙,顯式創(chuàng)建了一個(gè) TabController。
// 由于 TabController 需要一個(gè) TickerProvider (vsync 參數(shù))懂扼, 又混入了 SingleTickerProviderStateMixin禁荸。
// 由于 TabController 中會(huì)執(zhí)行動(dòng)畫,持有一些資源阀湿,所以在頁面銷毀時(shí)必須得釋放資源屡限。
class TabViewRoute1 extends StatefulWidget {
@override
_TabViewRoute1State createState() => _TabViewRoute1State();
}
class _TabViewRoute1State extends State<TabViewRoute1>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List tabs = ["新聞", "歷史", "圖片"];
@override
void initState() {
super.initState();
_tabController = TabController(length: tabs.length, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("App Name"),
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList(),
),
),
body: TabBarView( // 構(gòu)建
controller: _tabController,
children: tabs.map((e) {
return KeepAliveWrapper(
child: Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
),
);
}).toList(),
),
);
}
@override
void dispose() {
// 釋放資源
_tabController.dispose();
super.dispose();
}
}
滑動(dòng)頁面時(shí)頂部的 Tab 也會(huì)跟著動(dòng),點(diǎn)擊頂部 Tab 時(shí)頁面也會(huì)跟著切換炕倘。
實(shí)戰(zhàn)中,如果需要 TabBar 和 TabBarView 聯(lián)動(dòng)翰撑,通常會(huì)創(chuàng)建一個(gè) DefaultTabController 作為它們共同的父級(jí)組件罩旋,這樣它們?cè)趫?zhí)行時(shí)就會(huì)從組件樹向上查找啊央,都會(huì)使用指定的這個(gè) DefaultTabController。
修改后的實(shí)現(xiàn)如下:
class TabViewRoute2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
List tabs = ["新聞", "歷史", "圖片"];
return DefaultTabController(
length: tabs.length,
child: Scaffold(
appBar: AppBar(
title: Text("App Name"),
bottom: TabBar(
tabs: tabs.map((e) => Tab(text: e)).toList(),
),
),
body: TabBarView( // 構(gòu)建
children: tabs.map((e) {
return KeepAliveWrapper(
child: Container(
alignment: Alignment.center,
child: Text(e, textScaleFactor: 5),
),
);
}).toList(),
),
),
);
}
}
無需去手動(dòng)管理 Controller 的生命周期涨醋,也不需要提供 SingleTickerProviderStateMixin瓜饥,同時(shí)也沒有其他的狀態(tài)需要管理,也就不需要用 StatefulWidget 了浴骂。
3. CustomScrollView
多個(gè)可滾動(dòng)組件時(shí)滑動(dòng)效果是分離的(各自擁有獨(dú)立的Scrollable乓土、Viewport、Sliver)溯警。
需要一個(gè)"膠水"(CustomScrollView:創(chuàng)建一個(gè)共用的 Scrollable和Viewport對(duì)象趣苏,然后將各可滾動(dòng)組件對(duì)應(yīng)的Sliver添加到共用的Viewport對(duì)象中)把可滾動(dòng)組件"粘"起來使滑動(dòng)效果統(tǒng)一。
直接將ListView梯轻、GridView(本身是可滾動(dòng)組件而不是Sliver食磕,各自包含滾動(dòng)模型)作為Sliver是不行的,所以Flutter提供了可滾動(dòng)組件的Sliver版(如SliverList喳挑、SliverGrid等)彬伦。CustomScrollView的子組件必須都是Sliver。
列表Sliver
SliverList 列表(對(duì)應(yīng)ListView)
SliverFixedExtentList 高度固定的列表(對(duì)應(yīng)ListView伊诵,指定itemExtent時(shí))
SliverAnimatedList 添加/刪除列表項(xiàng)可以執(zhí)行動(dòng)畫(對(duì)應(yīng)AnimatedList)
SliverGrid 二維網(wǎng)格(對(duì)應(yīng)GridView)
SliverPrototypeExtentList 根據(jù)原型生成高度固定的列表(對(duì)應(yīng)ListView单绑,指定prototypeItem 時(shí))
SliverFillViewport 包含多個(gè)子組件,每個(gè)都可以填滿屏幕(對(duì)應(yīng)PageView)
布局曹宴、裝飾Sliver
SliverPadding (對(duì)應(yīng)Padding)
SliverVisibility搂橙、SliverOpacity (對(duì)應(yīng)Visibility、Opacity)
SliverFadeTransition (對(duì)應(yīng)FadeTransition)
SliverLayoutBuilder (對(duì)應(yīng)LayoutBuilder)
其他Sliver
SliverToBoxAdapter 一個(gè)適配器浙炼,可以將RenderBox適配為Sliver份氧。
SliverAppBar 對(duì)應(yīng)AppBar
SliverPersistentHeader 滑動(dòng)到頂部時(shí)可以固定住。
/*
SliverSafeArea({
Key? key,
this.left = true,
this.top = true,
this.right = true,
this.bottom = true,
this.minimum = EdgeInsets.zero,
required this.sliver,
})
*/
示例(SliverList)
SliverList(
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
示例(SliverAppBar弯屈、SliverPadding蜗帜、SliverGrid、SliverFixedExtentList)
import 'package:flutter/material.dart';
class CustomScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar( // AppBar资厉,包含一個(gè)導(dǎo)航欄
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Demo'),
background: Image.asset(
"./images/avatar.png", fit: BoxFit.cover,),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid( // Grid
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // 按兩列顯示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
),
// List
new SliverFixedExtentList( // 子元素高度50的列表
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
// 創(chuàng)建列表項(xiàng)
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 50 // 50個(gè)列表項(xiàng)
),
),
],
),
);
}
}
示例 (SliverFixedExtentList)
列表項(xiàng)高度相同時(shí)優(yōu)先使用:SliverFixedExtentList厅缺、SliverPrototypeExtentList。
Widget buildTwoSliverList() {
var listView = SliverFixedExtentList(
itemExtent: 56, // 列表項(xiàng)高度固定
delegate: SliverChildBuilderDelegate(
(_, index) => ListTile(title: Text('$index')),
childCount: 10,
),
);
return CustomScrollView(
slivers: [
listView,
listView,
],
);
}
- SliverToBoxAdapter組件(一個(gè)適配器:將RenderBox適配為Sliver)
不是所有組件都有Sliver版本宴偿,為此Flutter提供了SliverToBoxAdapter湘捎。
注意:
如果子組件可滑動(dòng)且滑動(dòng)方向和CustomScrollView一致,則不會(huì)正常工作窄刘。
原因:
CustomScrollView 組合 Sliver 的原理是為所有子 Sliver 提供一個(gè)共享的 Scrollable窥妇,然后統(tǒng)一處理指定滑動(dòng)方向的滑動(dòng)事件,如果 Sliver 中引入了其他的 Scrollable娩践,則滑動(dòng)事件便會(huì)沖突活翩。
Flutter中手勢(shì)沖突時(shí)烹骨,默認(rèn)的策略是子元素生效(即子元素處理后停止冒泡)。
解決:
使用NestedScrollView材泄。
示例(SliverToBoxAdapter)
CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SizedBox(
height: 300,
child: PageView(
children: [Text("1"), Text("2")],
),
),
),
buildSliverFixedList(),
],
);
- SliverAppBar(頂部時(shí)只顯示標(biāo)題沮焕,下滑后展開內(nèi)容)
1. floating
2. snap
手指放開時(shí)會(huì)根據(jù)當(dāng)前狀態(tài)決定是否展開或收起。
false:則導(dǎo)航欄會(huì)停留在上次滑動(dòng)位置拉宗。
3. pinned
滾動(dòng)到頂部后峦树,是否固定。默認(rèn)false(滾動(dòng)出頂部后導(dǎo)航欄將消失)旦事。
4. expandedHeight
導(dǎo)航欄展開后的高度魁巩。
5. flexibleSpace
擴(kuò)展彈性空間:導(dǎo)航欄滑動(dòng)時(shí)的收起/展開組件(可以有背景圖片和導(dǎo)航欄文字),當(dāng)滑動(dòng)到頂部后只顯示文字導(dǎo)航欄族檬,當(dāng)下滑后會(huì)逐步顯示背景內(nèi)容歪赢。
例
SliverAppBar _getAppBar(String title) {
return SliverAppBar(
pinned: true,
expandedHeight: 200,
brightness: Brightness.dark,
flexibleSpace: FlexibleSpaceBar(
title: Text(title),
background: Image.network(
'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=688497718,308119011&fm=26&gp=0.jpg',
fit: BoxFit.cover,
),
),
);
}
- SliverPersistentHeader(當(dāng)滑動(dòng)到頂部時(shí)將組件固定在頂部)
初衷是為了實(shí)現(xiàn)SliverAppBar,所以它的一些屬性和回調(diào)在SliverAppBar中才會(huì)用到单料。
SliverPersistentHeader({
Key? key,
// 構(gòu)造header組件的委托
required SliverPersistentHeaderDelegate delegate,
// header滑動(dòng)到可視區(qū)域頂部時(shí)是否固定在頂部
this.pinned = false,
// pinned為false 時(shí) 埋凯,則 header 可以滑出可視區(qū)域(不會(huì)固定到頂部),當(dāng)用戶再次向下滑動(dòng)時(shí)扫尖,不管 header已經(jīng)被滑出了多遠(yuǎn)白对,都會(huì)立即出現(xiàn)在可視區(qū)域頂部并固定住,直到繼續(xù)下滑到 header在列表中原來的位置時(shí)换怖,header才會(huì)重新回到原來的位置(不再固定在頂部)甩恼。
this.floating = false,
})
abstract class SliverPersistentHeaderDelegate {
// header 最大高度、最小高度
// pined為true時(shí)沉颂,當(dāng)header剛剛固定到頂部時(shí)条摸,此時(shí)會(huì)對(duì)它應(yīng)用 maxExtent (最大高度);當(dāng)用戶繼續(xù)往上滑動(dòng)時(shí)铸屉,header 的高度會(huì)隨著用戶繼續(xù)上滑從 maxExtent 逐漸減小到 minExtent钉蒲。
// 如果想讓header高度固定,則將 maxExtent 和 minExtent 指定為同樣的值即可彻坛。
double get maxExtent;
double get minExtent;
// 構(gòu)建 header顷啼。
// shrinkOffset:取值范圍[0,maxExtent],當(dāng)header剛剛到達(dá)頂部時(shí),shrinkOffset 值為maxExtent昌屉,如果用戶繼續(xù)向上滑動(dòng)列表钙蒙,shrinkOffset的值會(huì)隨著用戶滑動(dòng)的偏移減小,直到減到0時(shí)间驮。
// overlapsContent:只要有內(nèi)容和 Sliver 重疊時(shí)就應(yīng)該為 true躬厌。一般不建議使用,使用時(shí)一定要小心竞帽。
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
// header 是否需要重新構(gòu)建烤咧;通常當(dāng)父級(jí)的 StatefulWidget 更新狀態(tài)時(shí)會(huì)觸發(fā)偏陪。
// 一般來說只有當(dāng) Delegate 的配置發(fā)生變化時(shí),應(yīng)該返回false煮嫌,比如新舊的 minExtent、maxExtent等其他配置不同時(shí)需要返回 true抱虐,其余情況返回 false 即可昌阿。
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
// 下面這幾個(gè)屬性是SliverPersistentHeader在SliverAppBar中時(shí)實(shí)現(xiàn)floating、snap效果時(shí)會(huì)用到恳邀,開發(fā)過程很少用到懦冰。
TickerProvider? get vsync => null;
FloatingHeaderSnapConfiguration? get snapConfiguration => null;
OverScrollHeaderStretchConfiguration? get stretchConfiguration => null;
PersistentHeaderShowOnScreenConfiguration? get showOnScreenConfiguration => null;
}
示例(一個(gè)通用的委托構(gòu)造器)
typedef SliverHeaderBuilder = Widget Function(
BuildContext context, double shrinkOffset, bool overlapsContent);
class SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
// child 為 header
SliverHeaderDelegate({
required this.maxHeight,
this.minHeight = 0,
required Widget child,
}) : builder = ((a, b, c) => child),
assert(minHeight <= maxHeight && minHeight >= 0);
// 最大和最小高度相同
SliverHeaderDelegate.fixedHeight({
required double height,
required Widget child,
}) : builder = ((a, b, c) => child),
maxHeight = height,
minHeight = height;
// 需要自定義builder時(shí)使用
SliverHeaderDelegate.builder({
required this.maxHeight,
this.minHeight = 0,
required this.builder,
});
//
final double maxHeight;
final double minHeight;
final SliverHeaderBuilder builder;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
Widget child = builder(context, shrinkOffset, overlapsContent);
assert(() { // 測(cè)試代碼:如果在調(diào)試模式,且子組件設(shè)置了key谣沸,則打印日志
if (child.key != null) {
print('${child.key}: shrink: $shrinkOffset刷钢,overlaps:$overlapsContent');
}
return true;
}());
// 讓 header 盡可能充滿限制的空間;寬度為 Viewport 寬度乳附,高度隨著用戶滑動(dòng)在[minHeight,maxHeight]之間變化内地。
return SizedBox.expand(child: child);
}
@override
double get maxExtent => maxHeight;
@override
double get minExtent => minHeight;
@override
bool shouldRebuild(SliverHeaderDelegate old) {
return old.maxExtent != maxExtent || old.minExtent != minExtent;
}
}
=====================
使用
class PersistentHeaderRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
buildSliverList(),
SliverPersistentHeader(
pinned: true,
delegate: SliverHeaderDelegate(// 有最大和最小高度
maxHeight: 80,
minHeight: 50,
child: buildHeader(1),
),
),
buildSliverList(),
SliverPersistentHeader(
pinned: true,
delegate: SliverHeaderDelegate.fixedHeight( // 固定高度
height: 50,
child: buildHeader(2),
),
),
buildSliverList(20),
],
);
}
// 構(gòu)建固定高度的SliverList,count為列表項(xiàng)屬相
Widget buildSliverList([int count = 5]) {
return SliverFixedExtentList(
itemExtent: 50,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(title: Text('$index'));
},
childCount: count,
),
);
}
// 構(gòu)建 header
Widget buildHeader(int i) {
return Container(
color: Colors.lightBlue.shade200,
alignment: Alignment.centerLeft,
child: Text("PersistentHeader $i"),
);
}
}
注意:
1. 當(dāng)有多個(gè)SliverPersistentHeader時(shí)赋除,第一個(gè) SliverPersistentHeader 的 overlapsContent值會(huì)一直為 false阱缓。
- 自定義Sliver
Sliver布局協(xié)議
1. (當(dāng)Sliver進(jìn)入構(gòu)建區(qū)域時(shí))Viewport 將當(dāng)前布局和配置信息通過 SliverConstraints 傳遞給 Sliver。
2. Sliver 確定自身的布局举农、繪制等信息荆针,保存在 geometry 中(一個(gè) SliverGeometry 類型的對(duì)象)。
3. Viewport 讀取 geometry 中的信息來對(duì) Sliver 進(jìn)行布局和繪制颁糟。
/*
Sliver布局模型和盒布局模型
相同點(diǎn):
布局流程基本相同:父組件告訴子組件約束信息 > 子組件根據(jù)父組件的約束確定自生大小 > 父組件獲得子組件大小調(diào)整其位置航背。
不同點(diǎn):
1. 父組件傳遞給子組件的約束信息不同。盒模型傳遞的是 BoxConstraints棱貌,而 Sliver 傳遞的是 SliverConstraints玖媚。
2. 描述子組件布局信息的對(duì)象不同。盒模型的布局信息通過 Size 和 offset描述 键畴,而 Sliver的是通過 SliverGeometry 描述最盅。
3. 布局的起點(diǎn)不同。Sliver布局的起點(diǎn)一般是Viewport 起惕,而盒模型布局的起點(diǎn)可以是任意的組件涡贱。
*/
class SliverConstraints extends Constraints {
// 主軸方向
AxisDirection? axisDirection;
// Sliver 沿著主軸從列表的哪個(gè)方向插入?枚舉類型惹想,正向或反向
GrowthDirection? growthDirection;
// 用戶滑動(dòng)方向
ScrollDirection? userScrollDirection;
// 當(dāng)前Sliver理論上(可能會(huì)固定在頂部)已經(jīng)滑出可視區(qū)域的總偏移
double? scrollOffset;
// 當(dāng)前Sliver之前的Sliver占據(jù)的總高度问词,因?yàn)榱斜硎菓屑虞d,如果不能預(yù)估時(shí)嘀粱,該值為double.infinity
double? precedingScrollExtent;
// 上一個(gè) sliver 覆蓋當(dāng)前 sliver 的大小激挪,通常在 sliver 是 pinned/floating 或者處于列表頭尾時(shí)有效辰狡。
double? overlap;
// 當(dāng)前Sliver在Viewport中的最大可以繪制的區(qū)域。
// 繪制如果超過該區(qū)域會(huì)比較低效(因?yàn)椴粫?huì)顯示)
double? remainingPaintExtent;
// 縱軸的長度垄分;如果列表滾動(dòng)方向是垂直方向宛篇,則表示列表寬度。
double? crossAxisExtent;
// 縱軸方向
AxisDirection? crossAxisDirection;
// Viewport在主軸方向的長度薄湿;如果列表滾動(dòng)方向是垂直方向叫倍,則表示列表高度。
double? viewportMainAxisExtent;
// Viewport 預(yù)渲染區(qū)域的起點(diǎn)[-Viewport.cacheExtent, 0]
double? cacheOrigin;
// Viewport加載區(qū)域的長度豺瘤,范圍:
//[viewportMainAxisExtent,viewportMainAxisExtent + Viewport.cacheExtent*2]
double? remainingCacheExtent;
}
SliverGeometry({
// Sliver在主軸方向預(yù)估長度吆倦,大多數(shù)情況是固定值,用于計(jì)算sliverConstraints.scrollOffset
this.scrollExtent = 0.0,
this.paintExtent = 0.0, // 可視區(qū)域中的繪制長度
this.paintOrigin = 0.0, // 繪制的坐標(biāo)原點(diǎn)坐求,相對(duì)于自身布局位置
// 在 Viewport中占用的長度蚕泽;如果列表滾動(dòng)方向是垂直方向,則表示列表高度桥嗤。
// 范圍[0,paintExtent]
double? layoutExtent,
this.maxPaintExtent = 0.0,//最大繪制長度
this.maxScrollObstructionExtent = 0.0,
double? hitTestExtent, // 點(diǎn)擊測(cè)試的范圍
bool? visible,// 是否顯示
// 是否會(huì)溢出Viewport抖僵,如果為true潦牛,Viewport便會(huì)裁剪
this.hasVisualOverflow = false,
// scrollExtent的修正值:layoutExtent變化后赶诊,為了防止sliver突然跳動(dòng)(應(yīng)用新的layoutExtent)可以先進(jìn)行修正赤屋。
this.scrollOffsetCorrection,
double? cacheExtent, // 在預(yù)渲染區(qū)域中占據(jù)的長度
})
示例(頂部圖片可伸縮)
實(shí)現(xiàn)目標(biāo):頂部圖片只顯示一部分,當(dāng)用戶向下拽時(shí)圖片的剩余部分會(huì)逐漸顯示师逸。
實(shí)現(xiàn)思路:實(shí)現(xiàn)一個(gè) Sliver司倚,將它作為 CustomScrollView 的第一孩子,然后根據(jù)用戶的滑動(dòng)來動(dòng)態(tài)調(diào)整 Sliver 的布局和顯示篓像。
@override
Widget build(BuildContext context) {
return CustomScrollView(
// 為了能使CustomScrollView拉到頂部時(shí)還能繼續(xù)往下拉动知,必須讓 physics 支持彈性效果
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
// 自定義組件
SliverFlexibleHeader(
visibleExtent: 200,, // 初始狀態(tài)在列表中占用的布局高度
// 為了能根據(jù)下拉狀態(tài)變化來定制顯示的布局,通過一個(gè) builder 來動(dòng)態(tài)構(gòu)建布局员辩。
builder: (context, availableHeight, direction) {
return GestureDetector(
onTap: () => print('tap'), // 測(cè)試是否可以響應(yīng)事件
child: Image(
image: AssetImage("imgs/avatar.png"),
width: 50.0,
height: availableHeight,
alignment: Alignment.bottomCenter,
fit: BoxFit.cover,
),
);
},
),
// 構(gòu)建一個(gè)list
buildSliverList(30),
],
);
}
typedef SliverFlexibleHeaderBuilder = Widget Function(
BuildContext context,
double maxExtent,
//ScrollDirection direction,
);
class SliverFlexibleHeader extends StatelessWidget {
const SliverFlexibleHeader({
Key? key,
this.visibleExtent = 0,
required this.builder,
}) : super(key: key);
final SliverFlexibleHeaderBuilder builder;
final double visibleExtent;
@override
Widget build(BuildContext context) {
// 當(dāng)_SliverFlexibleHeader 中每次對(duì)子組件進(jìn)行布局時(shí)盒粮,都會(huì)觸發(fā) LayoutBuilder 來重新構(gòu)建子 widget ,LayoutBuilder 中收到的 constraints 就是 _SliverFlexibleHeader 中對(duì)子組件進(jìn)行布局時(shí) 傳入的 constraints奠滑。
return _SliverFlexibleHeader(
visibleExtent: visibleExtent,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return builder(
context,
constraints.maxHeight
);
},
),
);
}
}
// 要自定義 RenderObject丹皱,所以需要繼承 RenderObjectWidget
class _SliverFlexibleHeader extends SingleChildRenderObjectWidget {
const _SliverFlexibleHeader({
Key? key,
required Widget child,
this.visibleExtent = 0,
}) : super(key: key, child: child);
final double visibleExtent;
@override
RenderObject createRenderObject(BuildContext context) {
return _FlexibleHeaderRenderSliver(visibleExtent);
}
@override
void updateRenderObject(
BuildContext context, _FlexibleHeaderRenderSliver renderObject) {
renderObject..visibleExtent = visibleExtent;
}
}
class _FlexibleHeaderRenderSliver extends RenderSliverSingleBoxAdapter {
_FlexibleHeaderRenderSliver(double visibleExtent)
: _visibleExtent = visibleExtent;
double _lastOverScroll = 0;
double _lastScrollOffset = 0;
late double _visibleExtent = 0;
set visibleExtent(double value) {
// 可視長度發(fā)生變化,更新狀態(tài)并重新布局
if (_visibleExtent != value) {
_lastOverScroll = 0;
_visibleExtent = value;
markNeedsLayout();
}
}
@override
// 通過 Viewport 傳來的 SliverConstraints 結(jié)合子組件的高度宋税,最終確定了 _SliverFlexibleHeader 的布局摊崭、繪制等相關(guān)信息,它們被保存在了 geometry 中杰赛,之后呢簸,Viewport 就可以讀取 geometry 來確定 _SliverFlexibleHeader 在 Viewport 中的位置,然后進(jìn)行繪制。
void performLayout() {
// 滑動(dòng)距離大于_visibleExtent時(shí)則表示子節(jié)點(diǎn)已經(jīng)在屏幕之外了
if (child == null || (constraints.scrollOffset > _visibleExtent)) {
geometry = SliverGeometry(scrollExtent: _visibleExtent);
return;
}
// 測(cè)試overlap,下拉過程中overlap會(huì)一直變化.
double overScroll = constraints.overlap < 0 ? constraints.overlap.abs() : 0;
var scrollOffset = constraints.scrollOffset;
// 在Viewport中頂部的可視空間為該 Sliver 可繪制的最大區(qū)域根时。
// 1. 如果Sliver已經(jīng)滑出可視區(qū)域則 constraints.scrollOffset 會(huì)大于 _visibleExtent瘦赫,
// 這種情況我們?cè)谝婚_始就判斷過了。
// 2. 如果我們下拉超出了邊界蛤迎,此時(shí) overScroll>0确虱,scrollOffset 值為0,所以最終的繪制區(qū)域?yàn)? // _visibleExtent + overScroll.
double paintExtent = _visibleExtent + overScroll - constraints.scrollOffset;
// 繪制高度不超過最大可繪制空間
paintExtent = min(paintExtent, constraints.remainingPaintExtent);
// 對(duì)子組件進(jìn)行布局忘苛,子組件通過 LayoutBuilder可以拿到這里我們傳遞的約束對(duì)象(ExtraInfoBoxConstraints)
child!.layout(
constraints.asBoxConstraints(maxExtent: paintExtent),
parentUsesSize: false,
);
// 最大為_visibleExtent蝉娜,最小為 0
double layoutExtent = min(_visibleExtent, paintExtent);
// 設(shè)置geometry,Viewport 在布局時(shí)會(huì)用到
geometry = SliverGeometry(
scrollExtent: layoutExtent,
paintOrigin: -overScroll,
paintExtent: paintExtent,
maxPaintExtent: paintExtent,
layoutExtent: layoutExtent,
);
}
}
傳遞額外的布局信息
在實(shí)際使用 SliverFlexibleHeader 時(shí)扎唾,我們有時(shí)在構(gòu)建子 widget 時(shí)可能會(huì)依賴當(dāng)前列表的滑動(dòng)方向,當(dāng)然我們可以在 SliverFlexibleHeader 的 builder 中記錄前后的 availableHeight 的差來確定滑動(dòng)方向南缓,但是這樣比較麻煩胸遇,需要使用者來手動(dòng)處理。我們知道在滑動(dòng)時(shí)汉形,Sliver 的 SliverConstraints 中已經(jīng)包含了 userScrollDirection纸镊,如果我們能將它經(jīng)過統(tǒng)一的處理然后透?jìng)鹘o LayoutBuilder 的話就非常好好了,這樣就不需要開發(fā)者在使用時(shí)自己維護(hù)滑動(dòng)方向了概疆!按照這個(gè)思路我們來實(shí)現(xiàn)一下逗威。
首先我們遇到了第一個(gè)問題: LayoutBuilder 接收的參數(shù)我們沒法指定。兩種方案:
1. 我們知道在上面的場(chǎng)景中岔冀,在對(duì)子組件進(jìn)行布局時(shí)我們傳給子組件的約束只使用了最大長度凯旭,最小長度是沒有用到的使套,那么我們可以將滑動(dòng)方向通過最小長度傳遞給 LayoutBuilder侦高,然后再 LayoutBuilder 中取出即可嫉柴。
2(建議). 定義一個(gè)新類奉呛,讓它繼承自 BoxConstraints,然后再添加一個(gè)可以保存 scrollDirection 的屬性瞧壮。
方案 1 有一個(gè)副作用就是會(huì)影響子組件布局登馒。我們知道 LayoutBuilder 是在子組件 build 階段執(zhí)行的谊娇,當(dāng)我們?cè)O(shè)置了最小長度后,我們雖然在 build 階段沒有用到它赠堵,但是在子組件在布局階段仍然會(huì)應(yīng)用此約束法褥,所以最終還會(huì)影響子組件的布局半等。
按照方案 2 來實(shí)現(xiàn):定義一個(gè) ExtraInfoBoxConstraints 類杀饵,它可以攜帶約束之外的信息切距,為了盡可能通用谜悟,我們使用泛型:
class ExtraInfoBoxConstraints<T> extends BoxConstraints {
ExtraInfoBoxConstraints(
this.extra,
BoxConstraints constraints,
) : super(
minWidth: constraints.minWidth,
minHeight: constraints.minHeight,
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
);
// 額外的信息
final T extra;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ExtraInfoBoxConstraints &&
super == other &&
other.extra == extra;
}
@override
int get hashCode {
return hashValues(super.hashCode, extra);
}
}
重載了“==”運(yùn)算符葡幸,這是因?yàn)?Flutter 在布局期間在特定的情況下會(huì)檢測(cè)前后兩次 constraints 是否相等然后來決定是否需要重新布局蔚叨,所以我們需要重載“==”運(yùn)算符,否則可能會(huì)在最大/最小寬高不變但 extra 發(fā)生變化時(shí)不會(huì)觸發(fā) child 重新布局悄泥,這時(shí)也就不會(huì)觸發(fā) LayoutBuilder弹囚,這明顯不符合預(yù)期鸥鹉,因?yàn)槲覀兿M?extra 發(fā)生變化時(shí)庶骄,會(huì)觸發(fā) LayoutBuilder 重新構(gòu)建 child。
首先我們修改 __FlexibleHeaderRenderSliver 的 performLayout 方法:
...
//對(duì)子組件進(jìn)行布局灸异,子組件通過 LayoutBuilder可以拿到這里我們傳遞的約束對(duì)象(ExtraInfoBoxConstraints)
child!.layout(
ExtraInfoBoxConstraints(
direction, //傳遞滑動(dòng)方向
constraints.asBoxConstraints(maxExtent: paintExtent),
),
parentUsesSize: false,
);
...
然后修改 SliverFlexibleHeader 實(shí)現(xiàn),在 LayoutBuilder 中就可以獲取到滑動(dòng)方向:
typedef SliverFlexibleHeaderBuilder = Widget Function(
BuildContext context,
double maxExtent,
ScrollDirection direction,
);
class SliverFlexibleHeader extends StatelessWidget {
const SliverFlexibleHeader({
Key? key,
this.visibleExtent = 0,
required this.builder,
}) : super(key: key);
final SliverFlexibleHeaderBuilder builder;
final double visibleExtent;
@override
Widget build(BuildContext context) {
return _SliverFlexibleHeader(
visibleExtent: visibleExtent,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return builder(
context,
constraints.maxHeight,
// 獲取滑動(dòng)方向
(constraints as ExtraInfoBoxConstraints<ScrollDirection>).extra,
);
},
),
);
}
}
最后我們看一下 SliverFlexibleHeader 中確定滑動(dòng)方向的邏輯:
// 下拉過程中overlap會(huì)一直變化.
double overScroll = constraints.overlap < 0 ? constraints.overlap.abs() : 0;
var scrollOffset = constraints.scrollOffset;
_direction = ScrollDirection.idle;
// 根據(jù)前后的overScroll值之差確定列表滑動(dòng)方向。注意么伯,不能直接使用 constraints.userScrollDirection田柔,
// 這是因?yàn)樵搮?shù)只表示用戶滑動(dòng)操作的方向。比如當(dāng)我們下拉超出邊界時(shí)欣舵,然后松手邻遏,此時(shí)列表會(huì)彈回,即列表滾動(dòng)
// 方向是向上廷没,而此時(shí)用戶操作已經(jīng)結(jié)束颠黎,ScrollDirection 的方向是上一次的用戶滑動(dòng)方向(向下)滞项,這是便有問題文判。
var distance = overScroll > 0
? overScroll - _lastOverScroll
: _lastScrollOffset - scrollOffset;
_lastOverScroll = overScroll;
_lastScrollOffset = scrollOffset;
if (constraints.userScrollDirection == ScrollDirection.idle) {
_direction = ScrollDirection.idle;
_lastOverScroll = 0;
} else if (distance > 0) {
_direction = ScrollDirection.forward;
} else if (distance < 0) {
_direction = ScrollDirection.reverse;
}
高度修正 scrollOffsetCorrection
如果 visibleExtent 變化時(shí),可以看到有一個(gè)突兀地跳動(dòng)戏仓,這是因?yàn)?visibleExtent 變化時(shí)會(huì)導(dǎo)致 layoutExtent 發(fā)生變化赏殃,也就是說 SliverFlexibleHeader 在屏幕中所占的布局高度會(huì)發(fā)生變化仁热,所以列表就出現(xiàn)跳動(dòng)。但這個(gè)跳動(dòng)效果太突兀了思劳,我們知道每一個(gè) Sliver 的高度是通過 scrollExtent 屬性預(yù)估出來的敢艰,因此我們需要修正一下 scrollExtent钠导,但是我們不能直接修改 scrollExtent 的值森瘪,直接修改不會(huì)有任何動(dòng)畫效果扼睬,仍然會(huì)跳動(dòng)窗宇,為此军俊,SliverGeometry 提供了一個(gè) scrollOffsetCorrection 屬性粪躬,它專門用于修正 scrollExtent 镰官,我們只需要將要修正差值傳給scrollOffsetCorrection泳唠,然后 Sliver 會(huì)自動(dòng)執(zhí)行一個(gè)動(dòng)畫效果過渡到我們期望的高度警检。
// 是否需要修正scrollOffset. _visibleExtent 值更新后扇雕,
// 為了防止突然的跳動(dòng),要先修正 scrollOffset础淤。
double? _scrollOffsetCorrection;
set visibleExtent(double value) {
// 可視長度發(fā)生變化鸽凶,更新狀態(tài)并重新布局
if (_visibleExtent != value) {
_lastOverScroll = 0;
_reported = false;
// 計(jì)算修正值
_scrollOffsetCorrection = value - _visibleExtent;
_visibleExtent = value;
markNeedsLayout();
}
}
@override
void performLayout() {
// _visibleExtent 值更新后玻侥,為了防止突然的跳動(dòng)凑兰,先修正 scrollOffset
if (_scrollOffsetCorrection != null) {
geometry = SliverGeometry(
//修正
scrollOffsetCorrection: _scrollOffsetCorrection,
);
_scrollOffsetCorrection = null;
return;
}
...
}
邊界
在 SliverFlexibleHeader 構(gòu)建子組件時(shí)開發(fā)者可能會(huì)依賴“當(dāng)前的可用高度是否為0”來做一些特殊處理姑食,比如記錄是否子組件已經(jīng)離開了屏幕音半。但是根據(jù)上面的實(shí)現(xiàn)曹鸠,當(dāng)用戶滑動(dòng)非吵固遥快時(shí)叛薯,子組件離開屏幕時(shí)的最后一次布局時(shí)傳遞的約束的 maxExtent 可能不為 0耗溜,而當(dāng) constraints.scrollOffset 大于 _visibleExtent 時(shí)我們?cè)?performLayout 的一開始就返回了抖拴,因此 LayoutBuilder 的 builder 中就有可能收不到 maxExtent 為 0 時(shí)的回調(diào)腥椒。為了解決這個(gè)問題笼蛛,我們只需要在每次 Sliver 離開屏幕時(shí)調(diào)用一次 child.layout 同時(shí) 將maxExtent 指定為 0 即可往湿,為此我們修改一下:
void performLayout() {
if (child == null) {
geometry = SliverGeometry(scrollExtent: _visibleExtent);
return;
}
//當(dāng)已經(jīng)完全滑出屏幕時(shí)
if (constraints.scrollOffset > _visibleExtent) {
geometry = SliverGeometry(scrollExtent: _visibleExtent);
// 通知 child 重新布局领追,注意,通知一次即可棕孙,如果不通知蟀俊,滑出屏幕后欧漱,child 在最后
// 一次構(gòu)建時(shí)拿到的可用高度可能不為 0误甚。因?yàn)槭褂谜咴跇?gòu)建子節(jié)點(diǎn)的時(shí)候窑邦,可能會(huì)依賴
// "當(dāng)前的可用高度是否為0" 來做一些特殊處理冈钦,比如記錄是否子節(jié)點(diǎn)已經(jīng)離開了屏幕瞧筛,
// 因此较幌,我們需要在離開屏幕時(shí)確保LayoutBuilder的builder會(huì)被調(diào)用一次(構(gòu)建子組件)乍炉。
if (!_reported) {
_reported = true;
child!.layout(
ExtraInfoBoxConstraints(
_direction, //傳遞滑動(dòng)方向
constraints.asBoxConstraints(maxExtent: 0),
),
//我們不會(huì)使用自節(jié)點(diǎn)的 Size, 關(guān)于此參數(shù)更詳細(xì)的內(nèi)容見本書后面關(guān)于layout原理的介紹
parentUsesSize: false,
);
}
return;
}
//子組件回到了屏幕中,重置通知狀態(tài)
_reported = false;
...
}
示例2
SliverPersistentHeaderToBox槐瑞,可以將任意 RenderBox 適配為可以固定到頂部的 Sliver 而不用顯式指定高度随珠,同時(shí)避免overlapsContent問題窗看。
在沒有使用 SliverAppBar 時(shí)显沈,用 SliverPersistentHeaderToBox拉讯,如果使用了 SliverAppBar 魔慷,用SliverPersistentHeader院尔。
第一步: 實(shí)現(xiàn)SliverPersistentHeaderToBox
typedef SliverPersistentHeaderToBoxBuilder = Widget Function(
BuildContext context,
double maxExtent, //當(dāng)前可用最大高度
bool fixed, // 是否已經(jīng)固定
);
class SliverPersistentHeaderToBox extends StatelessWidget {
// 默認(rèn)構(gòu)造函數(shù)邀摆,直接接受一個(gè) widget栋盹,不用顯式指定高度
SliverPersistentHeaderToBox({
Key? key,
required Widget child,
}) : builder = ((a, b, c) => child),
super(key: key);
// builder 構(gòu)造函數(shù)例获,需要傳一個(gè) builder榨汤,同樣不需要顯式指定高度
SliverPersistentHeaderToBox.builder({
Key? key,
required this.builder,
}) : super(key: key);
final SliverPersistentHeaderToBoxBuilder builder;
@override
Widget build(BuildContext context) {
return _SliverPersistentHeaderToBox(
// 通過 LayoutBuilder接收 Sliver 傳遞給子組件的布局約束信息
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return builder(
context,
constraints.maxHeight,
//約束中需要傳遞的額外信息是一個(gè)bool類型,表示 Sliver 是否已經(jīng)固定到頂部
(constraints as ExtraInfoBoxConstraints<bool>).extra,
);
},
),
);
}
}
第二步:實(shí)現(xiàn) _SliverPersistentHeaderToBox
class _RenderSliverPersistentHeaderToBox extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
child!.layout(
ExtraInfoBoxConstraints(
//只要 constraints.scrollOffset不為0啼器,則表示已經(jīng)有內(nèi)容在當(dāng)前Sliver下面了端壳,即已經(jīng)固定到頂部了
constraints.scrollOffset != 0,
constraints.asBoxConstraints(
// 我們將剩余的可繪制空間作為 header 的最大高度約束傳遞給 LayoutBuilder
maxExtent: constraints.remainingPaintExtent,
),
),
//我們要根據(jù)child大小來確定Sliver大小损谦,所以后面需要用到child的大姓占瘛(size)信息
parentUsesSize: true,
);
// 子節(jié)點(diǎn) layout 后就能獲取它的大小了
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
geometry = SliverGeometry(
scrollExtent: childExtent,
paintOrigin: 0, // 固定闯参,如果不想固定應(yīng)該傳` - constraints.scrollOffset`
paintExtent: childExtent,
maxPaintExtent: childExtent,
);
}
// 重要鹿寨,必須重寫脚草。
@override
double childMainAxisPosition(RenderBox child) => 0.0;
}
上面代碼有四點(diǎn)需要注意:
1. constraints.scrollOffset 不為 0 時(shí)馏慨,則表示已經(jīng)固定到頂部了熏纯。
2. 在布局階段拿到子組件的 size 信息樟澜,然后通過通過子組件的大小來確定 Sliver 大兄确 (設(shè)置geometry)毒费。 這樣就不再需要我們顯式傳高度值了愈魏。
3. 通過給 paintOrigin 設(shè)為 0 來實(shí)現(xiàn)頂部固定效果;不固定到頂部時(shí)應(yīng)該傳 - constraints.scrollOffset。
4. 必須要重寫 childMainAxisPosition 牌柄,否則事件便會(huì)失效珊佣,該方法的返回值(paintOrigin 的位置)在“點(diǎn)擊測(cè)試”中會(huì)用到。
最后一步:測(cè)試
class SliverPersistentHeaderToBoxRoute extends StatelessWidget {
const SliverPersistentHeaderToBoxRoute({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
buildSliverList(5),
SliverPersistentHeaderToBox.builder(builder: headerBuilder),
buildSliverList(5),
SliverPersistentHeaderToBox(child: wTitle('Title 2')),
buildSliverList(50),
],
);
}
// 當(dāng) header 固定后顯示陰影
Widget headerBuilder(context, maxExtent, fixed) {
// 獲取當(dāng)前應(yīng)用主題
var theme = Theme.of(context);
return Material(
child: Container(
color: fixed ? Colors.white : theme.canvasColor,
child: wTitle('Title 1'),
),
elevation: fixed ? 4 : 0,
shadowColor: theme.appBarTheme.shadowColor,
);
}
// 我們約定小寫字母 w 開頭的函數(shù)代表是需要構(gòu)建一個(gè) Widget,這比 buildXX 會(huì)更簡潔
Widget wTitle(String text) =>
ListTile(title: Text(text), onTap: () => print(text));
}
創(chuàng)建兩個(gè) header:
第一個(gè) header:當(dāng)沒有滑動(dòng)到頂部時(shí)胸梆,外觀和正常列表項(xiàng)一樣碰镜;當(dāng)固定到頂部后绪颖,顯示一個(gè)陰影柠横。為了實(shí)現(xiàn)這個(gè)效果我們需要通過 SliverPersistentHeaderToBox.builder 來動(dòng)態(tài)創(chuàng)建。
第二個(gè) header: 一個(gè)普通的列表項(xiàng)搬俊,它接受一個(gè) widget唉擂。
4. NestedScrollView
如果CustomScrollView的子組件是一個(gè)可滾動(dòng)組件(通過SliverToBoxAdapter嵌入)且它們的滑動(dòng)方向一致時(shí)則會(huì)造成手勢(shì)沖突,不能正常工作空扎∽猓可使用NestedScrollView解決。
NestedScrollView({
...
required this.headerSliverBuilder, // 外部可滾動(dòng)組件:CustomScrollView
required this.body, // 內(nèi)部可滾動(dòng)組件:任意可滾動(dòng)組件
this.floatHeaderSlivers = false, //
})
示例
Material(
child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// 返回一個(gè) Sliver 數(shù)組給外部可滾動(dòng)組件勒魔。
return <Widget>[
SliverAppBar(
title: const Text('嵌套ListView'),
pinned: true, // 固定在頂部
forceElevated: innerBoxIsScrolled,
),
buildSliverList(5), // 構(gòu)建一個(gè) sliverList
];
},
body: ListView.builder(
padding: const EdgeInsets.all(8),
physics: const ClampingScrollPhysics(), // 重要
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return SizedBox(
height: 50,
child: Center(child: Text('Item $index')),
);
},
),
),
);