赞
踩
之前介绍了布局和容器,它们都用于摆放一个或多个子组件,而实际应用中,受限于手机、Pad、电脑的屏幕大小,一个布局不可能摆放无限个组件,我们往往采取滚动的方式,来使得一部分组件展示在屏幕上,一部分组件处于缓存中,像这种方式的布局,我们叫作可滚动布局
Flutter中可滚动布局基本都来自Sliver
模型,原理和安卓传统UI的ListView、RecyclerView类似,滚动布局里面的每个子组件的样式往往是相同的,由于组件占用内存较大,所以在内存上我们可以缓存有限个组件,滚动布局时仅仅刷新组件的数据,来达到滚动布局存放无限个子组件的目标
SingleChildScrollView
比较特殊,是基于Box
模型的可滚动布局,只接受一个子组件,由于没有复用机制,我们一般用于如长文本这种有限滚动距离的情况,构造如下:
const SingleChildScrollView({
super.key,
this.scrollDirection = Axis.vertical,// 可滚动方向
this.reverse = false, // 反向
this.padding,// 内间距
this.primary,// 是否顶层
this.physics,// 滚动的摩擦力等
this.controller,// 控制器,可用于控制滚动到指定位置
this.child,
this.dragStartBehavior = DragStartBehavior.start,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,// 键盘消失方式
})
简单使用:
Container(
width: 200,
height: 300,
color: Colors.amber,
child: SingleChildScrollView(
child: Text("hi,flutter " * 100),
)
);
效果:
ListView
是很常用的滚动布局,构造如下:
ListView({ super.key, super.scrollDirection, super.reverse, super.controller, super.primary, super.physics, super.shrinkWrap,// 是否根据子组件的总长度来设置ListView的长度 默认false super.padding, this.itemExtent,// 固定item的宽高,垂直滚动时为高,水平时为宽。固定后性能好 this.prototypeItem,// 和itemExtent类似,只不过测量依据是一个组件 bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, super.cacheExtent,// 预渲染区域长度 List<Widget> children = const <Widget>[], int? semanticChildCount, super.dragStartBehavior, super.keyboardDismissBehavior, super.restorationId, super.clipBehavior, })
简单使用:
Iterable<int> generateInts() sync* {
for (var i = 0; i < 100; i++) {
yield i;
}
}
Container(
width: 200,
height: 50,
color: Colors.amber,
child: ListView(
children: generateInts().map((e) => Text("hi,flutter $e")).toList(),
)
);
效果:
命名式构造**ListView.builder
**,通过itemCount
参数来表示内部一共有多少元素,通过itemBuilder
参数来构造子组件,类似安卓RecyclerView的ItemType,我们可以方便的通过逻辑处理构造出不同类型的组件:
Container( width: 200, height: 300, child: ListView.builder( itemBuilder: (context, index) { if (index % 2 == 0) { return Container( color: Colors.amber, child: Text("偶数:$index"), ); } else { return Container( color: Colors.black12, child: Text("奇数:$index"), ); } }, itemCount: 100, ) );
效果:
**ListView.separated
比ListView.builder
**多出一个参数separatorBuilder
,表示每个元素之间的一个分割组件
Container( width: 200, height: 300, child: ListView.separated( itemBuilder: (context, index) { if (index % 2 == 0) { return Container( color: Colors.amber, child: Text("偶数:$index"), ); } else { return Container( color: Colors.black12, child: Text("奇数:$index"), ); } }, itemCount: 100, separatorBuilder: (context, index) { return Divider( height: 3, color: Colors.black, ); }, ) );
效果:
滚动组件都有一个controller
参数,类型为**ScrollController
**,用来控制滚动,构造如下:
ScrollController({
double initialScrollOffset = 0.0,// 初始滚动偏移
this.keepScrollOffset = true,// 是否保存滚动位置
this.debugLabel,
})
下面通过一个按钮来控制滚动,需要用到状态:
class _MyScroll extends State<MyScroll> { ScrollController _controller = ScrollController(); double _offset = 0; @override void initState() { _controller.addListener(() { print(_controller.offset); // 打印滚动偏移 }); } @override void dispose() { _controller.dispose(); } @override Widget build(BuildContext context) { return Container( width: 200, height: 300, child: Stack( children: [ ListView.separated( controller: _controller, itemBuilder: (context, index) { if (index % 2 == 0) { return Container( color: Colors.amber, child: Text("偶数:$index"), ); } else { return Container( color: Colors.black12, child: Text("奇数:$index"), ); } }, itemCount: 100, separatorBuilder: (context, index) { return Divider( height: 3, color: Colors.black, ); }, ), Positioned( child: FloatingActionButton( onPressed: () { _offset += 200; _controller.animateTo(_offset, duration: Duration(milliseconds: 200), curve: Curves.linear); }, ), right: 0, bottom: 0, ), ], ), ); } }
效果:
ScrollController
是支持一对多的,当一个ScrollController
绑定多个滚动布局时,如果相对某个可滚动布局单独操作,可以使用ScrollController
的positions
参数,该参数为一组ScrollPosition
,一个ScrollPosition
对应一个滚动布局
_controller.positions.elementAt(0).animateTo(to, duration: duration, curve: curve);
使用NotificationListener
是另一种监听滚动事件的方式,ScrollController
只能够监听滚动的位置,但NotificationListener
还可以获取ViewPort
(滚动布局中,用于渲染当前视口中需要显示的Sliver
)的一些信息,通过ViewPort
信息我们可以知道可滚动的最大距离等信息,值得注意的是NotificationListener
可以处于滚动布局到View树根中任意位置,构造比较简单:
const NotificationListener({
super.key,
required super.child,
this.onNotification,
})
下面通过NotificationListener
计算进度,并显示在FAB上:
class _MyScroll extends State<MyScroll> { ScrollController _controller = ScrollController(); double _offset = 0; int _progress = 0; @override void initState() { _controller.addListener(() { print(_controller.offset); // 打印滚动偏移 _offset = _controller.offset; }); } @override void dispose() { _controller.dispose(); } @override Widget build(BuildContext context) { return Container( width: 200, height: 300, child: NotificationListener( onNotification: (ScrollNotification notification) { double progress = notification.metrics.pixels / notification.metrics.maxScrollExtent; //重新构建 setState(() { _progress = (progress * 100).toInt(); }); return false; }, child: Stack( children: [ ListView.separated( controller: _controller, itemBuilder: (context, index) { if (index % 2 == 0) { return Container( color: Colors.amber, child: Text("偶数:$index"), ); } else { return Container( color: Colors.black12, child: Text("奇数:$index"), ); } }, itemCount: 100, separatorBuilder: (context, index) { return Divider( height: 3, color: Colors.black, ); }, ), Positioned( child: FloatingActionButton( child: Text("$_progress%"), onPressed: () { _offset += 200; _controller.animateTo(_offset, duration: Duration(milliseconds: 200), curve: Curves.linear); }, ), right: 0, bottom: 0, ), ], ), ), ); } }
ListView
一行只能摆放一个子组件,GridView
可以指定一行摆放多个子组件,比较特殊的参数为gridDelegate
,需要我们传入一个SliverGridDelegate
对象,SliverGridDelegate
是抽象类,实现类有SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithFixedCrossAxisCount
就是固定个数摆放,构造如下:
const SliverGridDelegateWithFixedCrossAxisCount({
required this.crossAxisCount,// 横轴摆放子组件个数
this.mainAxisSpacing = 0.0,// 主轴方向(可滚动方向)的间距
this.crossAxisSpacing = 0.0,// 横轴方向子元素的间距
this.childAspectRatio = 1.0,// 子元素在横轴长度和主轴长度的比例
this.mainAxisExtent,
})
简单使用:
Container( width: 200, height: 300, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 20, mainAxisSpacing: 30), children: [ Container( padding: const EdgeInsets.all(8), color: Colors.green[100], child: const Text('1'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[200], child: const Text('2'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[300], child: const Text('3'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[400], child: const Text('4'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[500], child: const Text('5'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[600], child: const Text('6'), ), ], ), );
效果:
SliverGridDelegateWithMaxCrossAxisExtent
只是将固定数量改为固定最大宽度,内部会自动进行计算,得出每个item的固定宽度,每个item宽度依然是相同的
Container( width: 200, height: 300, child: GridView( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 120, crossAxisSpacing: 20, mainAxisSpacing: 30), children: [ Container( padding: const EdgeInsets.all(8), color: Colors.green[100], child: const Text('1'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[200], child: const Text('2'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[300], child: const Text('3'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[400], child: const Text('4'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[500], child: const Text('5'), ), Container( padding: const EdgeInsets.all(8), color: Colors.green[600], child: const Text('6'), ), ], ), );
效果:
和ListView
一样,GridView
的命名式构造builder
,参数itemBuilder
允许用户自己根据数据来构建不同的组件:
Container( width: 200, height: 300, child: GridView.builder( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 120, crossAxisSpacing: 20, mainAxisSpacing: 30), itemBuilder: (BuildContext context, int index) { return Container( padding: const EdgeInsets.all(8), color: index % 2 == 0 ? Colors.green[100] : Colors.amber[100], child: Text('$index'), ); }, itemCount: 100, ), );
效果:
具有页面切换效果的组件,你可以横向切换页面,也可以竖向切换,常常用在首页。构造如下:
PageView({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, PageController? controller,// 控制page切换的控制器 this.physics, this.pageSnapping = true,// 每次滑动是否强制切换整个页面 this.onPageChanged,// 页面变化的回调 List<Widget> children = const <Widget>[], this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, this.restorationId, this.clipBehavior = Clip.hardEdge, this.scrollBehavior, this.padEnds = true, })
使用上很简单,传入一个组件列表即可:
PageView(
children: [
Center(
child: Text("1"),
),
Center(
child: Text("2"),
),
Center(
child: Text("3"),
),
],
);
效果:
PageView
默认每次切换都会重新build子组件,如果需要缓存页面,可以查看:可滚动组件子项缓存
TabBarView
封装了PageView
,来达到与TabBar
的联动效果,TabBarView
构造如下:
const TabBarView({
super.key,
required this.children,
this.controller,// TabController 控制页面切换与TabBar设置相同的TabController达到联动效果
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
this.viewportFraction = 1.0,
this.clipBehavior = Clip.hardEdge,
})
TabBar
构造如下:
const TabBar({ super.key, required this.tabs,// tab组件集合 this.controller,// TabController 与TabBarView设置相同达到联动效果 this.isScrollable = false, this.padding, this.indicatorColor,// 指示器颜色 this.automaticIndicatorColorAdjustment = true, this.indicatorWeight = 2.0,// 指示器高度 this.indicatorPadding = EdgeInsets.zero,// 指示器padding this.indicator,// 指示器 Decoration类型 this.indicatorSize, // 指示器长度,tab长度|label长度 this.dividerColor,// 分隔符颜色 this.labelColor,// label的文本颜色 this.labelStyle,// label的TextStyle this.labelPadding,// label的padding this.unselectedLabelColor, this.unselectedLabelStyle, this.dragStartBehavior = DragStartBehavior.start, this.overlayColor,// 焦点、悬停、水波纹颜色 this.mouseCursor,// 鼠标光标 this.enableFeedback,// 提供声学和/或触觉反馈 this.onTap,// 点击了tab的回调 this.physics,// 滚动的物理效果,摩擦力等 this.splashFactory,// 水波纹效果 this.splashBorderRadius,// 水波纹Radius })
Tab
是Flutter为TabBar
提供的一个选项组件,构造如下:
const Tab({
super.key,
this.text,// 文本
this.icon,// 图标
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,// 高度
this.child,// 自定义子组件
})
TabController
构造如下:
TabController({ int initialIndex = 0, Duration? animationDuration, required this.length, required TickerProvider vsync})
TabController
必传两个参数,length
代表page的总数,tabs
与page
的数量要相等,vsync
是动画执行过程的TickerProvider
上下文,可以在定义TabController
时使用模板类SingleTickerProviderStateMixin
结合上面三种组件使用:
class _MyScroll extends State<MyScroll> with SingleTickerProviderStateMixin { late TabController _tabController; @override void initState() { // length表示page的总数 _tabController = TabController(length: 3, vsync: this); } @override void dispose() { // 释放资源 _tabController.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( bottom: TabBar( controller: _tabController, indicatorSize: TabBarIndicatorSize.label, tabs: const [ Tab(text: "home"), Tab(text: "message"), Tab(text: "mine"), ], ), ), body: TabBarView( controller: _tabController, children: [ Container( alignment: Alignment.center, color: Colors.amber, child: const Text("home"), ), Container( alignment: Alignment.center, color: Colors.cyan, child: const Text("message"), ), Container( alignment: Alignment.center, color: Colors.deepPurpleAccent, child: const Text("mine"), ), ], ), ); } }
效果:
上面我们使用了Flutter内置的Sliver
模型布局,针对大量数据达到复用组件,以提高性能,覆盖了大多数应用场景
实际上Sliver
模型布局在Flutter中分成三个角色:
Sliver
:复用机制的核心,按需进行构建和布局
ViewPort
:滚动布局中,用于渲染当前视口中需要显示的Sliver
Scrollable
:监听到用户滑动行为后,根据最新的滑动偏移构建 Viewport
上面的滚动布局这三大角色都是1:1:1的,但对于一些特殊的需要组合滚动布局的情况,Flutter也提供了CustomScrollView
组件,创建一个公共的 Scrollable
和 Viewport
,然后它的 slivers
参数接受一个 Sliver
数组,来达到组合多个滚动布局的效果
Sliver所有的组件可以查看官方文档:Sliver相关组件
通过选择不同的Sliver组件,我们也可以很方便的打造官方提供的常用滚动布局,下面是我们使用过的相关的Sliver:
Sliver名称 | 功能 | 对应的可滚动组件 |
---|---|---|
SliverList | 列表 | ListView |
SliverFixedExtentList | 高度固定的列表 | ListView,指定itemExtent 时 |
SliverAnimatedList | 添加/删除列表项可以执行动画 | AnimatedList |
SliverGrid | 网格 | GridView |
SliverPrototypeExtentList | 根据原型生成高度固定的列表 | ListView,指定prototypeItem 时 |
SliverFillViewport | 包含多个子组件,每个都可以填满屏幕 | PageView |
也有专门针对Sliver的容器:
Sliver名称 | 对应 Box |
---|---|
SliverPadding | Padding |
SliverVisibility、SliverOpacity | Visibility、Opacity |
SliverFadeTransition | FadeTransition |
SliverLayoutBuilder | LayoutBuilder |
其他Sliver:
Sliver名称 | 说明 |
---|---|
SliverAppBar | 对应 AppBar,主要是为了在 CustomScrollView 中使用。 |
SliverToBoxAdapter | 一个适配器,可以将 Box 适配为 Sliver。 |
SliverPersistentHeader | 滑动到顶部时可以固定住。 |
CustomScrollView
构造的参数也都是介绍过的:
const CustomScrollView({ super.key, super.scrollDirection, super.reverse, super.controller, super.primary, super.physics, super.scrollBehavior, super.shrinkWrap, super.center,// slivers中用key选定一个中心组件作为中心轴,其他子组件的滚动方向和摆放以中心轴为准进行正方向还是反方向 super.anchor,// 锚点,效果为初始化时离中心轴的反向滚动距离,距离=整体高度*锚点值。不设置center的情况下,就是一个顶部留白 super.cacheExtent, this.slivers = const <Widget>[], super.semanticChildCount, super.dragStartBehavior, super.keyboardDismissBehavior, super.restorationId, super.clipBehavior, })
下面我们使用CustomScrollView
将一个SliverList
和SliverGrid
进行组合:
class _MyScroll extends State<MyScroll> with SingleTickerProviderStateMixin { GlobalKey _centerKey = GlobalKey(); @override Widget build(BuildContext context) { return CustomScrollView( anchor: 0.5,// 初始距离中心轴半个整体组件的高度 center: _centerKey,// 以SliverGrid为中心轴 slivers: [ SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 50, ), ), SliverGrid( key: _centerKey, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Text("$index"); }, childCount: 50, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 50, ), ), ], ); } }
效果:
可滚动的AppBar
,参数和AppBar
基本一致,随着滚动偏移量分别不同操作,如进行缩放到固定位置和扩展变高,在安卓中为AppBarLayout结合CollapsingToolbarLayout使用的效果,构造如下:
const SliverAppBar({ super.key, this.leading,// title左侧子组件 this.automaticallyImplyLeading = true,//如果leading为null,是否自动实现默认的leading按钮 this.title, this.actions,// 导航栏右侧菜单 this.flexibleSpace,// 弹性空间,配合滚动,达到展开和固定的切换 this.bottom, // 导航栏底部菜单,通常为Tab按钮组 this.elevation,// 导航栏阴影 ... this.expandedHeight,// 展开高度 this.floating = false,// 用户向SliverAppBar滚动时,SliverAppBar是否应立即变为可见 this.pinned = false,// 滚动到SliverAppBar视图是否应在继续滚动时保持可见。 this.snap = false,// 如果[snap]和[foating]为true,向SliverAppBar滚动时SliverAppBar具有浮动效果 this.stretch = false,// SliverAppBar是否应该拉伸以填充滚动区域。 this.stretchTriggerOffset = 100.0, this.onStretchTrigger, this.shape, this.toolbarHeight = kToolbarHeight, this.leadingWidth, @Deprecated( 'This property is obsolete and is false by default. ' 'This feature was deprecated after v2.4.0-0.0.pre.', ) this.backwardsCompatibility, this.toolbarTextStyle, this.titleTextStyle, this.systemOverlayStyle, })
简单使用:
Material( child: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( title: const Text('hi title'), background: Image.asset( "./drawable/img.png", fit: BoxFit.cover, ), ), // floating: true, pinned: true, // snap: true, // stretch: true, ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 50, ), ), ], ), );
效果:
SliverPersistentHeader
可以达到粘性标题的效果,构造如下:
const SliverPersistentHeader({
super.key,
required this.delegate,
this.pinned = false,// 滚动到Header组件视图时是否固定显示
this.floating = false,// 用户反向滚动时,是否应立即变为可见
})
delegate
参数为SliverPersistentHeaderDelegate类型,SliverPersistentHeaderDelegate是一个抽象类,需要自己实现:
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { // header 最大高度;pined为 true 时,当 header 刚刚固定到顶部时高度为最大高度。 @override double get maxExtent; // header 的最小高度; @override double get minExtent; // 构建组件 @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { throw UnimplementedError(); } // 重新构建的条件 @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { throw UnimplementedError(); } }
我们简单封装后:
class MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { double maxHeight; double minHeight; Widget Function( BuildContext context, double shrinkOffset, bool overlapsContent) layoutBuild; // header 最大高度;pined为 true 时,当 header 刚刚固定到顶部时高度为最大高度。 @override double get maxExtent => maxHeight; // header 的最小高度; @override double get minExtent => minHeight; MySliverPersistentHeaderDelegate( {required this.maxHeight, this.minHeight = 0, required this.layoutBuild}); // 构建组件 @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) => layoutBuild(context, shrinkOffset, overlapsContent); // 重新构建的条件 @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { return oldDelegate.maxExtent != maxExtent || oldDelegate.minExtent != minExtent; } }
在CustiomScrollView中使用SliverPersistentHeader:
Material( child: CustomScrollView( slivers: [ SliverAppBar( expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( title: const Text('hi title'), background: Image.asset( "./drawable/img.png", fit: BoxFit.cover, ), ), pinned: true, ), // SliverPersistentHeader SliverPersistentHeader( pinned: true, delegate: MySliverPersistentHeaderDelegate( maxHeight: 100, minHeight: 50, layoutBuild: (BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( height: 100, child: Text("hi"), color: Colors.amber, ); }, ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 50, ), ), ], ), );
效果:
CustomScrollView
下的Sliver集合,必须是Sliver组件,不能够使用Box模型,如果想要使用,可以通过SliverToBoxAdapter
,它将Box模型适配为Sliver模型
Material( child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Container( height: 300, child: PageView( children: [ Container( alignment: Alignment.center, child: Text("1"), ), Container( alignment: Alignment.center, child: Text("2"), ), ], ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 50, ), ), ], ), );
效果:
SliverToBoxAdapter
不能解决滑动冲突问题,由于CustomScrollView
组件,创建一个公共的 Scrollable
和 Viewport
,而SliverToBoxAdapter
下PageView
为一个单独的Scrollable
,Flutter中滚动事件优先分配给子组件,当两个Scrollable
存在并方向相同时,就会产生冲突
NestedScrollView
也是一个CustomScrollView
,NestedScrollView
与CustomScrollView
不同的是,它做了内部协调,将滚动区域分为head
和body
,参数headerSliverBuilder
接收一个函数,返回组件集合代表滚动区域的头部
Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverAppBar(title: Text("hi")), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( height: 50, alignment: Alignment.center, child: Text("$index"), ); }, childCount: 5, ), ), ]; }, body: ListView.builder( itemBuilder: (BuildContext context, int index) { return Container( alignment: Alignment.center, height: 60, child: Text("$index"), ); }, itemCount: 100, )), );
效果:
可以看到下面的body滑动会有一个水波纹效果,IOS则是一个弹性效果,如果想要去除效果,往下面看
最后贴下官方的示例,其中原先为了解决SliverAppBar设置floating
时,滚动展开导致遮挡body的冲突,官方给出了SliverOverlapAbsorber和SliverOverlapInjector组合使用的解决方案,现在这个组合的作用是SliverAppBar展开和缩小的动画效果、以及去除水波纹和IOS的弹性效果:
final List<String> tabs = <String>['Tab 1', 'Tab 2']; DefaultTabController( length: tabs.length, child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ SliverOverlapAbsorber( // This widget takes the overlapping behavior of the SliverAppBar, // and redirects it to the SliverOverlapInjector below. If it is // missing, then it is possible for the nested "inner" scroll view // below to end up under the SliverAppBar even when the inner // scroll view thinks it has not been scrolled. // This is not necessary if the "headerSliverBuilder" only builds // widgets that do not overlap the next sliver. handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: const Text('Books'), // This is the title in the app bar. pinned: true, expandedHeight: 150.0, // The "forceElevated" property causes the SliverAppBar to show // a shadow. The "innerBoxIsScrolled" parameter is true when the // inner scroll view is scrolled beyond its "zero" point, i.e. // when it appears to be scrolled below the SliverAppBar. // Without this, there are cases where the shadow would appear // or not appear inappropriately, because the SliverAppBar is // not actually aware of the precise position of the inner // scroll views. forceElevated: innerBoxIsScrolled, bottom: TabBar( // These are the widgets to put in each tab in the tab bar. tabs: tabs.map((String name) => Tab(text: name)).toList(), ), ), ), ]; }, body: TabBarView( // These are the contents of the tab views, below the tabs. children: tabs.map((String name) { // SafeArea适配避开屏幕顶部的状态栏和ios底部操作凹口 return SafeArea( top: false, bottom: false, child: Builder( // This Builder is needed to provide a BuildContext that is // "inside" the NestedScrollView, so that // sliverOverlapAbsorberHandleFor() can find the // NestedScrollView. builder: (BuildContext context) { return CustomScrollView( // The "controller" and "primary" members should be left // unset, so that the NestedScrollView can control this // inner scroll view. // If the "controller" property is set, then this scroll // view will not be associated with the NestedScrollView. // The PageStorageKey should be unique to this ScrollView; // it allows the list to remember its scroll position when // the tab view is not on the screen. key: PageStorageKey<String>(name), slivers: <Widget>[ SliverOverlapInjector( // This is the flip side of the SliverOverlapAbsorber // above. handle: NestedScrollView.sliverOverlapAbsorberHandleFor( context), ), SliverPadding( padding: const EdgeInsets.all(8.0), // In this example, the inner scroll view has // fixed-height list items, hence the use of // SliverFixedExtentList. However, one could use any // sliver widget here, e.g. SliverList or SliverGrid. sliver: SliverFixedExtentList( // The items in this example are fixed to 48 pixels // high. This matches the Material Design spec for // ListTile widgets. itemExtent: 48.0, delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { // This builder is called for each child. // In this example, we just number each list item. return ListTile( title: Text('Item $index'), ); }, // The childCount of the SliverChildBuilderDelegate // specifies how many children this inner list // has. In this example, each tab has a list of // exactly 30 items, but this is arbitrary. childCount: 30, ), ), ), ], ); }, ), ); }).toList(), ), ), ), );
效果:
本文借鉴:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。