前言
在上一篇文章中,我们了解了Flutter Slivers中SliverToBoxAdapter
、SliverPersistentHeader
、SliverFixedExtentList
等常用的组件。本章则是一些不那么常用的Slivers组件,大家仅需快速阅读一遍,有个印象即可,方便遇到一些业务时,能快速定位到需要使用的组件。
SliverLayoutBuilder
在基于Box协议的组件布局中,经常会使用LayoutBuilder
,因为可以通过它获取在布局过程中父组件的约束,然后就可以根据约束信息动态的创建布局。例如:
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth < 200) {
// 最大宽度小于200,显示一张图片
return Image.network("");
} else {
return Container();
}
},
),
那么在slivers中对应的就是SliverLayoutBuilder
,可以通过SliverLayoutBuilder
创建一个折叠的sliver widget树,根据组件的约束条件提供不同子组件。
CustomScrollView(
slivers: [
SliverLayoutBuilder(
builder: (BuildContext context, SliverConstraints constraints) {
if (constraints.userScrollDirection == ScrollDirection.forward) {
_color = Colors.blue; //向下滑动显示蓝色
} else if (constraints.userScrollDirection ==
ScrollDirection.idle) {
_color = Colors.yellow; //正常显示黄色
} else {
_color = Colors.purple; //向上滑动显示紫色
}
return SliverToBoxAdapter(
child: Container(
height: 200,
color: _color,
),
);
}),
SliverList(
delegate:
SliverChildBuilderDelegate(childCount: 20, (ctx, index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
height: 50,
);
}))
],
),
SliverAnimatedList
在一个有很多子Item的列表中,如果用户需要删除或添加事件,如果没有任何过渡动画,那么很有可能会让用户混淆刚刚发生的事情,不知道列表item是否添加或删除了。所以为了解决这样的问题,可以使用AnimatedContainer
这样带动画的组件,而在Slivers中,可以使用SliverAnimatedList
,通过SliverAnimatedListState
用于动态插入item
或删除item
。让我们通过一个例子来学习SliverAnimatedList
。
添加SliverAnimatedList
这里_listKey
是一个SliverAnimatedListStateGlobalKey
类型。该GlobalKey
有助于插入和删除列表项。initialItemCount
是定义列表初始化有几个item
。itemBuilder
是需要延迟构建的组件。
final GlobalKey<SliverAnimatedListState> _listKey =
GlobalKey<SliverAnimatedListState>();
SliverAnimatedList(
key: _listKey,
initialItemCount: _list.length,
itemBuilder: _buildItem,
),
再来看下_buildItem
:该index
参数指示项目在列表中的位置。参数的值index
将介于0和initialItemCount
加上已插入insertItem
的项目总数和减去已删除removeItem
的项目总数。
Widget _buildItem(
BuildContext context, int index, Animation<double> animation) {
return CardItem(
animation: animation,
item: _list[index],
selected: _selectedItem == _list[index],
onTap: () {
setState(() {
_selectedItem = _selectedItem == _list[index] ? null : _list[index];
});
},
);
}
实现将插入列表的_insert函数和从列表中删除的_remove函数
void _insert() {
final int index =
_selectedItem == null ? _list.length : _list.indexOf(_selectedItem!);
//添加
_list.insert(index, _nextItem++);
//更新UI
_listKey.currentState!.insertItem(index);
}
void _remove() {
final int index = _selectedItem == null
? _list.length - 1
: _list.indexOf(_selectedItem!);
_list.removeAt(index);
//之所以需要这个方法,是因为移除的item在动画完成之前都是可见的。
_listKey.currentState!.removeItem(
index,
(context, animation) => SizeTransition(
sizeFactor: animation,
child: Card(
child: Center(
child: Text(
'Item $index',
style: Theme.of(context).textTheme.headline4,
),
),
),
),
);
setState(() {
_selectedItem = null;
});
}
具体代码详见:点击跳转github详情代码地址
SliverOpacity
使用Opacity
可以使组件变得透明,而在slivers
中可以使用SliverOpacity
。它们的不同之处只在于:Opacity
适用于基于Box
协议的组件,SliverOpacity
适用于基于sliver
协议的组件。
可控制属性有:
opacity:它通常取值介于0.0到之间1.0。当值为 0.0,则不会绘制 sliver child
,若值为1.0,则会立即绘制 sliver child
。除了0.0和1.0之外,使用其他的值是非常昂贵的,例如0.5,因为需要将sliver child
绘制到缓冲区中。
alwaysIncludeSemantics:是否总是包含语义信息,默认是 false。该属性主要是用于辅助访问的,对于视障人员来说会更友好。如果属性的值是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读)。
使用也非常简单:
bool _isVisible = true;
...
CustomScrollView(
slivers: [
SliverOpacity(
opacity: _isVisible ? 1.0 : 0.0,
sliver: SliverList(
delegate: SliverChildListDelegate(
[
Container(
height: 50,
color: Colors.green,
child: Center(
child: Text(
"Taxze & SliverOpacity",
style: TextStyle(fontSize: 20, color: Colors.white),
),
),
),
],
),
),
),
SliverPadding(
padding: EdgeInsets.only(top: 24),
sliver: SliverToBoxAdapter(
child: FloatingActionButton(
child: const Icon(Icons.flip),
onPressed: () {
setState(() {
_isVisible = !_isVisible;
});
},
),
),
)
],
),
SliverAnimatedOpacity
如果使用SliverOpacity
,那么组件的不透明度会直接从0.0跳到1.0,转变的过程看起来不光滑。为了让Opacity
平滑的过渡,可以使用SliverAnimatedOpacity
。
bool _visible = true;
CustomScrollView(
slivers: [
SliverAnimatedOpacity(
//动画执行完毕
onEnd: () => print("动画完成"),
curve: Curves.linear,
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(seconds: 2),
sliver: SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Image.network(
"https://p3-passport.byteimg.com/img/user-avatar/af5f7ee5f0c449f25fc0b32c050bf100~180x180.awebp");
}, childCount: 1),
itemExtent: 200.0),
),
SliverPadding(
padding: EdgeInsets.only(top: 18),
sliver: SliverToBoxAdapter(
child: FloatingActionButton(
onPressed: () {
setState(() {
_visible = !_visible;
});
},
tooltip: 'Toggle opacity',
child: const Icon(Icons.flip),
)),
),
],
),
SliverFadeTransition
这也是使用opacity
为sliver
组件设置动画的。唯一的区别是,使用Animation<double>
值而不是double
值。
class _SliverPart3SliverFadeTransitionState
extends State<SliverPart3SliverFadeTransition>
with SingleTickerProviderStateMixin {
late final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
late final Animation<double> animation = CurvedAnimation(
parent: controller,
curve: Curves.easeIn,
);
@override
void initState() {
super.initState();
animation.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return CustomScrollView(slivers: <Widget>[
SliverAppBar(
title: Text('SliverPart3SliverFadeTransition'),
pinned: true,
),
SliverFadeTransition(
opacity: animation,
sliver: SliverToBoxAdapter(
child: Center(
child: Image.network(
"https: //p3-passport.byteimg.com/img/user-avatar/af5f7ee5f0c449f25fc0b32c050bf100~180x180.awebp",
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
),
))
]);
}
}
SliverVisibility
SliverVisibility
与Visibility
组件相同,只是它是基于sliver
协议的组件。
SliverVisibility
有许多的参数,如:
- maintainAnimation:用于判断
animations
不可见时是否保持在sliver
子树内。 - maintainInteractivity : 用于判断
sliver
隐藏时是否允许交互。 - maintainSemantics : 用于确定隐藏时是否保持
sliver
的语义。 - maintainSize:用于确定是否为
sliver
所在的位置保留空间。 - maintainState : 用于判断不可见时是否维护
sliver
子树的State
对象。 - replacementSliver:当
sliver
不可见时,此处定义的 Widget 可见。
一个简单的例子来了解SliverVisibility:
bool _visible = true;
CustomScrollView(
slivers: <Widget>[
SliverVisibility(
visible: _visible,
replacementSliver: SliverToBoxAdapter(
child: Container(
width: double.infinity,
height: 300,
color: Colors.brown,
child: Center(
child: Text(
"Image is hidden. Tap below button to show",
style: TextStyle(color: Colors.white),
),
),
),
),
sliver: SliverToBoxAdapter(
child: Image.network(
"https://p3-passport.byteimg.com/img/user-avatar/af5f7ee5f0c449f25fc0b32c050bf100~180x180.awebp",
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
)),
SliverPadding(
padding: EdgeInsets.only(top: 18),
sliver: SliverToBoxAdapter(
child: FloatingActionButton(
onPressed: () {
setState(() {
_visible = !_visible;
});
},
tooltip: 'Toggle visibility',
child: Icon(!_visible ? Icons.visibility : Icons.visibility_off),
)),
),
],
),
SliverIgnorePointer
如果希望sliver
忽略点击或其他任何类型的交互时,只需将sliver
组件包裹在里面SliverIgnorePointer
。它将禁用该小部件的交互。
当ignoring
属性为true
时,组件不可交互。当ignoringSemantics
属性为true时,子树将对Semantics
(语义)层不可见。
就用SliverVisibility
组件的例子,只需要在控制按钮上,包裹上一层SliverIgnorePointer
即可忽略点击事件。
SliverIgnorePointer(
sliver: SliverPadding(
padding: EdgeInsets.only(top: 18),
sliver: SliverToBoxAdapter(
child: FloatingActionButton(
onPressed: () {
setState(() {
_visible = !_visible;
});
},
tooltip: 'Toggle visibility',
child:
Icon(!_visible ? Icons.visibility : Icons.visibility_off),
)),
),
),
SliverSafeArea
如果你曾经使用过SafeArea
,那么SliverSafeArea
除了它基于sliver协议外,它也在做同样的事情。例如当你想要显示一篇长文章时,便可使用SliverSafeArea
来避免 iPhone X 上的缺口或其他类似的。
SliverSafeArea(sliver: SliverList(
delegate: SliverChildBuilderDelegate((ctx, index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
height: 100,
);
},childCount: 20),
))
尾述
三篇文章,从为什么要使用Sliver,再根据使用频率逐个解析Slivers系列的组件。相信您已经入门了Sliver的世界。那么在下一章,将单独详解NestedScrollView
,包括SliverOverlapAbsorber
和 SliverOverlapInjector
...的优化