滚动组件 Scrollable
是诸如 ListView
、CustomScrollView
、SingleChildScrollView
等常用的 widget 的父类。在这篇文章中,我们,我们将试着深入了解其中的内部机制。
首先,我们从滚动通知开始。
1. 通知是什么?可以将通知向上发送至部件树。Flutter 会在滚动、大小变化和布局变化等事件时发送通知。
换句话说,每当任何东西滚动或改变大小时,父元素会被通知。
我们来看看当我们在滚动时的通知里面都有些什么。
NotificationListener<滚动通知>(
onNotification: (notification) {
return false; // <- 在此处设置断点
},
child: ListView(
children: [
const SizedBox(height: 1000),
],
),
)
首先,我们要注意scrollDelta
。这是相对于前一状态新增的滚动位置像素数。如果是正数,表示用户正在向前滚动;如果是负数,表示用户正在向后滚动。
如果我们深入查看metrics
的内容,会发现很多有用的数据。让我们把这些数据可视化了。
滚动内容显示为红色,而固定组件则为蓝色。
不随滚动变化的值显示在图片的左边,动态的则在右边。
因此,extentTotal
是 Scrollable
内容的总高度。maxScrollExtent
是超出视窗的内容高度,scrollDelta
是自上次通知以来滚动的像素数,extentBefore
对应从开始到 Scrollable
的剩余高度,extentAfter
对应从结束到 Scrollable
的剩余高度。
viewPortDimension
是包含 Scrollable
的 widget,表示其高度。
我们把 SizedBox
的高度从 1000 改成 200 吧。现在,由于高度变化,因为没有滚动——滚动内容比视窗小,所以不会发送通知。如果我们要用到这些值,该怎么获取它们呢?
class _ScrollableExampleState extends State<ScrollableExample> {
final _controller = 滚动控制器(); // <-- 这里添加了一行
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_controller.position; // <-- 在这里可以设置断点
});
}
ListView(
controller: _controller, // <-- 这里添加了一行
...
)
}
正如我们看到的,视口高度与extentTotal
相同,而maxScrollExtent
为0,这是因为在这种情况下,无法进行滚动。
如果我们回到通知对象的话,我们会发现有一个叫做 dragDetails
的属性。这个对象和 GestureDetector
更新回调 中发送的对象很像。我们来在屏幕上滑动一下,看看发送的是什么数据:
打印("$scrollDelta\t$dragDetails");
# 打印滚动偏移量和拖动详情
让我们快速地滑动一下这个小部件,看看这些数据:
从第一眼看上去,scrollDelta
和 dragDetails.y
这两个值看起来像是相反的,但这些值看起来很接近。检查最后一个,你能猜出这里发生了什么?提示:这在 Android 上面是这样做的?
让我们在 iPhone 上试一试,看看结果。
拖拽数据类似,但 scrollDelta
有所不同。拖拽结束后,仍然会继续发送通知,但是没有包含拖拽更新的具体信息。
当然啦,原因是默认在Scrollable
中使用了不同的ScrollPhysics
。在iOS上默认使用BouncingScrollPhysics
,而在Android上则使用ClampingScrollPhysics
。
在BouncingScrollPhysics和ClampingScrollPhysics之间
4. ScrollPhysics 是怎么工作的?- 接收拖动信息和滚动位移
- 通过simulation 层进行处理过程
- 如有需要,则应用边界
- 输出滚动的位置和速度
- 可滚动组件会将计算出的值作为通知发送给监听者
在 Flutter 中,你可以选择不同的物理效果或创建自定义的物理效果。此外,还可以同时应用多个滚动物理效果,如下所示:
弹性的滚动物理(BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()))
这个示例将首先使用 BouncingScrollPhysics
,接着使用 AlwaysScrollableScrollPhysics
。
不,有时我们还需要做另外的事情,比如计算每个项目的大小。例如,使用 ListView.builder
时,项目的大小根据其内容来确定,例如 Text 小部件的高度可能是1行或2行。这样的情况。
在这种情况下,无法计算 totalExtent
,因为这意味着我们必须计算整个列表,这违背了懒加载的目的。在这种情况下,Flutter只会实例化视口内的项目和 cacheExtent
之内的项目。需要注意的是,即使一个项目只能部分适应 cacheExtent
,它仍然会被实例化。其中 cacheExtent
是 ListView
的一个参数。
当内容的大小小于父部件时,视口尺寸的计算有两种选择。
将 shrinkWrap
设置为 true
,渲染器会根据子元素来计算 viewPort
的高度。这将会关闭懒加载功能,这可能会导致性能问题。仅在你确认列表中的项目不会太多时使用此设置。
SingleChildScrollView(
child: Column(
children: [
Text("内容"),
const Spacer(), // <- 不要这样用
ElevatedButton(onPressed: () {}, child: Text("按钮"))
],
),
)
这里有个问题:Spacer
想尽可能多地占用空间,而Scrollable
让子组件随心所欲地占用空间。比如,Spacer
需要知道父组件的大小来计算其高度,但这行不通,因为父组件的尺寸依赖于子组件。
LayoutBuilder(builder: (context, constraints) {
// 布局构建器示例
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
maxHeight: double.infinity,
),
child: IntrinsicHeight(
child: Column(
children: [
Text("内容文本"),
const Spacer(), // 间距组件
ElevatedButton(onPressed: () {}, child: Text("按钮组件"))
],
),
),
),
);
})
LayoutBuilder
:
- 获取父元素的约束
- 提供尺寸信息
SingleChildScrollView
:
- 当内容超出显示范围时,允许滚动查看
约束框
:
- 设置最小高度为父元素的高度
- 允许最大高度为无限大
- 防止列塌陷现象
固有高度
:
- 强制正确计算列的高度
- 帮助调整子元素的大小
专栏
- 将孩子们竖直排列
- 扩展到最大范围
所以,我们可以在一个 Scrollable
中使用 Spacer
或 Flexible
您也可以使用此包中的ScrollableColumn
组件:此包。
让我们实现一个滚动监听事件,这样在滚动时就能更新应用栏中的视图了。
我们先从创建一个StatefulWidget
开始吧。它也可以是无状态的,这取决于你采用的状态管理方式。为了使这个例子尽可能简单,我只把一个ValueNotifier
作为StatefulWidget
的属性。
class _ScrollableZoomerState extends State<ScrollableZoomer> {
ValueNotifier<double> scrollPosition = ValueNotifier(0.0);
...
}
它将存储一个标准化的滚动位置值,其中 0.0 表示起点,1.0 表示完全滚动到底。
@override
// 该方法用于构建一个包含滚动监听的Scaffold
Widget build(BuildContext context) {
// 创建一个NotificationListener监听滚动更新
return Scaffold(
appBar: ..., // 略...
body: NotificationListener<ScrollUpdateNotification>(
onNotification: (notification) {
// 设置滚动位置为最小值与滚动更新度量的像素值除以最大滚动范围的较小值
scrollPosition.value = min(1, notification.metrics.pixels / notification.metrics.maxScrollExtent);
// 返回true表示继续监听
return true;
},
child: widget.child,
),
);
}
很简单,只需将滚动的像素数除以最大滚动距离,并确保其不超过100%。然后对下一步的按钮应用一些简单的转换。
appBar: AppBar(
actions: [
ValueListenableBuilder(
valueListenable: scrollPosition,
builder: (context, value, _) => Opacity(
opacity: value > 0.1 ? value : 0,
child: Transform.translate(
offset: Offset(0, 40 * (1 - value)),
child: TextButton(
onPressed: ...,
child: Text("下一页"),
),
),
),
)
],
),
你可以试试不同的数值和数学运算,唯一限制的就是你的想象力。
希望这篇文章对你有帮助。我会在找到更多信息时及时更新。可以关注我在Twitter上的动态,获取最新更新。