ScrollControllerCallback
arduino
// 示例假设:
// TrackingScrollController _trackingScrollController = TrackingScrollController();
/// 当 [ScrollController] 添加或移除一个 [ScrollPosition] 时的回调签名。
///
/// 由于 [ScrollPosition] 只有在 [Scrollable] 构建完成后才会创建并附加到控制器,
/// 因此可以用它来响应位置被附加到控制器的事件。
///
/// 通过直接访问该位置,可以对滚动位置的某些方面应用额外的监听器,
/// 比如 [ScrollPosition.isScrollingNotifier]。
///
/// 该回调由 [ScrollController.onAttach] 和 [ScrollController.onDetach] 使用。
typedef ScrollControllerCallback = void Function(ScrollPosition position);
这个注释解释了 ScrollControllerCallback
类型的作用,即当一个 ScrollPosition
被附加或从一个 ScrollController
移除时,触发的回调函数。
ScrollController
scala
/// 控制可滚动的 Widget。
///
/// 滚动控制器通常作为成员变量存储在 [State] 对象中,并在每次 [State.build] 时重用。
/// 一个滚动控制器可以用于控制多个可滚动的 Widget,
/// 但是某些操作,比如读取滚动的 [offset],要求控制器只能与单个可滚动的 Widget 一起使用。
///
/// 滚动控制器会创建一个 [ScrollPosition] 来管理与单个 [Scrollable] Widget 相关的状态。
/// 如果想使用自定义的 [ScrollPosition],可以通过继承 [ScrollController] 并重写 [createScrollPosition] 来实现。
///
/// {@macro flutter.widgets.scrollPosition.listening}
///
/// 通常与 [ListView]、[GridView]、[CustomScrollView] 一起使用。
///
/// 另请参阅:
///
/// * [ListView]、[GridView]、[CustomScrollView],它们可以由 [ScrollController] 控制。
/// * [Scrollable],是一个更底层的 Widget,它会创建并将 [ScrollPosition] 对象与 [ScrollController] 对象关联。
/// * [PageController],一个类似的对象,用于控制 [PageView]。
/// * [ScrollPosition],管理单个滚动 Widget 的滚动偏移量。
/// * [ScrollNotification] 和 [NotificationListener],可以用来监听滚动事件,而不需要使用 [ScrollController]。
class ScrollController extends ChangeNotifier {
/// 为可滚动的 Widget 创建一个控制器。
ScrollController({
double initialScrollOffset = 0.0,
this.keepScrollOffset = true,
this.debugLabel,
this.onAttach,
this.onDetach,
}) : _initialScrollOffset = initialScrollOffset {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
}
}
}
initialScrollOffset
dart
/// 用于 [offset] 的初始值。
///
/// 新创建并附加到此控制器的 [ScrollPosition] 对象将其偏移量初始化为此值,
/// 如果 [keepScrollOffset] 为 false 或者滚动偏移量尚未保存。
///
/// 默认为 0.0。
double get initialScrollOffset => _initialScrollOffset;
final double _initialScrollOffset;
描述了 initialScrollOffset
的作用,它是 ScrollController
的一个属性,表示初始的滚动偏移量。它会用于新创建并附加到控制器的 ScrollPosition
对象,只有在以下两种情况下,它才会被设置为这个初始值:
keepScrollOffset
为false
。- 滚动偏移量尚未被保存。
默认值是 0.0
,即初始时滚动视图的偏移量为 0。
keepScrollOffset
arduino
/// 每次滚动完成时,将当前的滚动 [offset] 保存到 [PageStorage],
/// 并在该控制器的可滚动控件被重新创建时恢复该偏移量。
///
/// 如果此属性设置为 false,则滚动偏移量不会被保存,
/// 并且始终使用 [initialScrollOffset] 来初始化滚动偏移量。
/// 如果设置为 true(默认值),则初始滚动偏移量会在控制器的可滚动控件第一次创建时使用,
/// 因为那时还没有需要恢复的滚动偏移量。之后保存的偏移量会被恢复,
/// 此时 [initialScrollOffset] 会被忽略。
///
/// 另请参阅:
///
/// * [PageStorageKey],当同一路由中有多个可滚动控件时,应该使用它来区分用于保存滚动偏移量的 [PageStorage] 位置。
final bool keepScrollOffset;
它决定了是否将滚动偏移量保存到 PageStorage
中,并在重新创建可滚动控件时恢复该偏移量。如果该属性为 true
(默认值),控件的滚动位置会在控件重建时恢复;如果为 false
,则总是使用 initialScrollOffset
来初始化滚动位置,而不会保存偏移量。
onAttach
perl
/// 当 [ScrollPosition] 附加到滚动控制器时调用。
///
/// 由于在 [Scrollable] 实际构建之前,滚动位置是不会被附加的,
/// 因此可以用它来响应新的滚动位置被附加的事件。
///
/// 在滚动位置附加时,像 [ScrollMetrics.maxScrollExtent] 这样的 [ScrollMetrics] 还不可用。
/// 这些信息直到 [Scrollable] 完成布局并计算出内容的完整范围后才会确定。
/// 可以使用 [ScrollPosition.hasContentDimensions] 来知道何时可以获取这些度量,
/// 或者可以使用 [ScrollMetricsNotification],下面会进一步讨论。
///
/// {@tool dartpad}
/// 此示例展示了如何使用 [ScrollController.onAttach] 为 [ScrollPosition.isScrollingNotifier] 添加监听器,
/// 用于在滚动发生时更改 [AppBar] 的颜色。
///
/// ** 请参阅 examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart 中的代码 **
/// {@end-tool}
final ScrollControllerCallback? onAttach;
描述了 onAttach
回调的作用,它会在一个 ScrollPosition
被附加到 ScrollController
时触发。这对于响应新附加的滚动位置很有用。
特别地,在 ScrollPosition
被附加时,ScrollMetrics
还未准备好(例如,maxScrollExtent
),因为这些值在 Scrollable
完成布局并计算内容的完整范围后才会确定。可以通过 ScrollPosition.hasContentDimensions
来确认这些度量值是否已准备好,或者使用 ScrollMetricsNotification
来监听这些度量值。
onDetach
arduino
/// 当 [ScrollPosition] 从滚动控制器中分离时调用。
///
/// {@tool dartpad}
/// 此示例展示了如何使用 [ScrollController.onAttach] 和 [ScrollController.onDetach] 为
/// [ScrollPosition.isScrollingNotifier] 添加监听器。
/// 该监听器用于在滚动发生时更改 [AppBar] 的颜色。
///
/// ** 请参阅 examples/api/lib/widgets/scroll_position/scroll_controller_on_attach.0.dart 中的代码 **
/// {@end-tool}
final ScrollControllerCallback? onDetach;
描述了 onDetach
回调的作用,它会在一个 ScrollPosition
从 ScrollController
中分离时触发。这个回调通常用来响应滚动位置被分离的事件。
同样,注释中还给出了一个示例,展示了如何使用 onAttach
和 onDetach
监听 ScrollPosition.isScrollingNotifier
,在滚动发生时改变 AppBar
的颜色。
positions
ini
/// 当前附加的滚动位置。
///
/// 不应直接修改此属性。[ScrollPosition] 对象可以通过 [attach] 和 [detach] 方法
/// 添加或移除。
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[];
定义了一个 getter 方法 positions
,它返回当前附加到控制器的所有 ScrollPosition
对象的列表。注释强调了不应该直接修改这个列表,而应该通过 attach
和 detach
方法来添加或移除 ScrollPosition
。
hasClients
ini
/// 是否有任何 [ScrollPosition] 对象通过 [attach] 方法附加到
/// [ScrollController]。
///
/// 如果为 false,则必须避免调用与 [ScrollPosition] 交互的成员,
/// 例如 [position]、[offset]、[animateTo] 和 [jumpTo]。
bool get hasClients => _positions.isNotEmpty;
定义了一个 hasClients
属性,它检查是否有任何 ScrollPosition
对象已经通过 attach
方法附加到 ScrollController
。如果没有附加任何 ScrollPosition
(即 hasClients
为 false
),则不应调用与滚动位置相关的功能,比如 position
、offset
、animateTo
或 jumpTo
,因为这些操作依赖于有效的 ScrollPosition
。
position
dart
/// 返回附加的 [ScrollPosition],通过它可以获取 [ScrollView] 的实际滚动偏移量。
///
/// 只有在仅附加了一个滚动位置时,调用此方法才是有效的。
ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
assert(_positions.length == 1, 'ScrollController 附加了多个滚动视图。');
return _positions.single;
}
定义了一个 position
属性,它返回附加的 ScrollPosition
对象,进而可以获取滚动视图的实际滚动偏移量。注释说明:
- 只有当 仅有一个 滚动位置被附加时,这个属性才是有效的。
- 使用
assert
来确保在调用position
时,_positions
列表中仅包含一个滚动位置,如果没有附加任何滚动位置或者附加了多个滚动位置,则会触发断言错误。
这个属性用于访问当前滚动视图的实际滚动位置。
offset(非常重要)
arduino
/// 当前可滚动 Widget 的滚动偏移量。
///
/// 需要确保控制器正在控制一个且仅一个可滚动的 Widget。
double get offset => position.pixels;
定义了 offset
属性,它返回当前滚动视图的滚动偏移量(即 position.pixels
)。需要注意的是,该属性要求控制器只能控制一个可滚动的 Widget。如果控制器控制了多个可滚动视图,调用该属性时会出现问题。
animateTo
dart
/// 将当前位置从当前值动画到给定的值。
///
/// 任何活动的动画都会被取消。如果用户当前正在滚动,该操作也会被取消。
///
/// 返回的 [Future] 会在动画结束时完成,无论是成功完成还是被提前中断。
///
/// 动画会在以下情况下被中断:
/// - 用户尝试手动滚动时。
/// - 启动了另一个活动时。
/// - 动画到达视口边缘并尝试超出滚动范围时。
/// (如果 [ScrollPosition] 不进行超滚动,而是允许滚动超出边界,
/// 那么超出边界将不会中断动画。)
///
/// 动画不受视口或内容尺寸变化的影响。
///
/// 动画完成后,滚动位置将尝试开始一个弹性活动,以防其值不稳定
/// (例如,如果滚动超出了边界,通常会有回弹效果)。
///
/// 动画持续时间不能为零。如果需要跳转到特定值而不使用动画,
/// 请使用 [jumpTo]。
///
/// 在小部件测试中调用 [animateTo] 时,`await` 返回的 [Future] 可能会导致测试挂起并超时。
/// 请改为使用 [WidgetTester.pumpAndSettle]。
Future<void> animateTo(double offset, {required Duration duration, required Curve curve}) async {
assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
await Future.wait<void>(<Future<void>>[
for (int i = 0; i < _positions.length; i += 1)
_positions[i].animateTo(offset, duration: duration, curve: curve),
]);
}
代码描述了 animateTo
方法的功能,它用于将滚动位置动画地移动到指定的偏移量 offset
。以下是其主要要点:
- 动画取消:任何正在进行的动画会被取消,用户滚动或其他活动也会中断当前动画。
- 返回
Future
:动画结束时返回一个Future
,无论动画是否成功完成,都会触发Future
的完成。 - 动画中断条件:动画会在几种情况下中断,例如手动滚动、启动其他活动或尝试超出滚动边界。
- 动画稳定性:动画结束后,如果滚动位置不稳定(如超出边界),会触发回弹动画。
- 动画时长 :
duration
必须大于零。如果想要立即跳转到目标位置,可以使用jumpTo
方法。 - 小部件测试 :在小部件测试中,直接
await
动画的Future
可能导致超时错误。推荐使用WidgetTester.pumpAndSettle
代替。
jumpTo
arduino
/// 将滚动位置从当前值直接跳转到给定值,
/// 不使用动画,也不检查新值是否在范围内。
///
/// 任何活动的动画都会被取消。如果用户当前正在滚动,该操作也会被取消。
///
/// 如果此方法更改了滚动位置,将会触发一系列的开始/更新/结束滚动通知。
/// 但无法通过此方法生成超滚动通知。
///
/// 跳转后,立即启动一个弹性活动,以防值超出了滚动范围。
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController 没有附加到任何滚动视图。');
for (final ScrollPosition position in List<ScrollPosition>.of(_positions)) {
position.jumpTo(value);
}
}
代码定义了 jumpTo
方法,用于将滚动位置 立即跳转 到指定的值,且不会有动画效果,也不会检查目标值是否在允许的滚动范围内。以下是关键点:
- 取消动画:如果有正在进行的动画,它会被取消。
- 取消滚动:如果用户正在手动滚动,也会取消当前的滚动操作。
- 滚动通知:如果滚动位置发生变化,会触发一系列滚动通知(开始、更新、结束),但不会生成超滚动通知。
- 弹性活动:跳转后会启动一个弹性活动,防止值超出范围时出现不稳定的滚动行为。
总之,jumpTo
允许直接跳转到目标位置,适用于需要立即定位的场景,但不会执行平滑过渡或边界检查。
attach
scss
/// 将给定的滚动位置注册到此控制器。
///
/// 当此方法返回后,[animateTo] 和 [jumpTo] 方法将操作给定的滚动位置。
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
onAttach?.call(position);
}
定义了 attach
方法,它将一个 ScrollPosition
对象注册到 ScrollController
上。以下是其主要要点:
- 注册位置 :首先,它会确保该
ScrollPosition
尚未添加到控制器中(通过断言)。 - 将位置添加到列表 :将
ScrollPosition
添加到_positions
列表中,意味着控制器开始管理这个位置。 - 添加监听器 :为该
ScrollPosition
添加一个监听器,以便控制器可以通知相关变化。 - 触发
onAttach
回调 :如果提供了onAttach
回调,它将被调用,并传入当前的ScrollPosition
。
总之,attach
方法用于将一个滚动位置与控制器关联,并为该位置添加必要的监听器,以便控制器可以通过方法如 animateTo
或 jumpTo
来操作它。
detach
scss
/// 从此控制器中注销给定的滚动位置。
///
/// 当此方法返回后,[animateTo] 和 [jumpTo] 方法将不再操作给定的滚动位置。
void detach(ScrollPosition position) {
assert(_positions.contains(position));
onDetach?.call(position);
position.removeListener(notifyListeners);
_positions.remove(position);
}
定义了 detach
方法,用于将一个 ScrollPosition
对象从 ScrollController
中注销。以下是其主要要点:
- 断言检查 :首先,通过断言检查该
ScrollPosition
是否已经附加到控制器中。 - 触发
onDetach
回调 :如果提供了onDetach
回调,它将被调用,并传入当前的ScrollPosition
。 - 移除监听器 :将该
ScrollPosition
的监听器移除,停止控制器对该位置的监听。 - 从列表中移除位置 :将
ScrollPosition
从_positions
列表中移除,意味着控制器不再管理这个位置。
总之,detach
方法用于将一个滚动位置与控制器的关联解除,并停止控制器对该位置的操作和监听。
dispose
scss
@override
void dispose() {
for (final ScrollPosition position in _positions) {
position.removeListener(notifyListeners);
}
super.dispose();
}
dispose
方法的重写,用于在销毁 ScrollController
时清理资源。以下是其主要步骤:
- 移除监听器 :遍历
_positions
列表中的每个ScrollPosition
对象,调用removeListener(notifyListeners)
移除对控制器的监听。这确保了控制器在销毁时不会继续监听滚动位置的变化。 - 调用父类的
dispose
方法 :调用super.dispose()
,以确保父类(ChangeNotifier
)的dispose
方法也会被调用,执行必要的资源释放。
dispose
方法用于释放 ScrollController
所持有的资源,尤其是移除所有 ScrollPosition
对象的监听器,以避免内存泄漏或不必要的操作
createScrollPosition
php
/// 为 [Scrollable] 小部件创建一个 [ScrollPosition]。
///
/// 子类可以重写此函数来自定义其控制的可滚动小部件使用的 [ScrollPosition]。
/// 例如,[PageController] 重写了此函数,返回一个面向页面的滚动位置子类,
/// 它可以在可滚动小部件调整大小时保持相同的页面可见。
///
/// 默认情况下,返回一个 [ScrollPositionWithSingleContext]。
///
/// 参数通常会传递给正在创建的 [ScrollPosition]:
///
/// * `physics`:一个 [ScrollPhysics] 实例,决定了 [ScrollPosition] 应如何响应用户交互,
/// 以及如何在释放或快速滑动时模拟滚动等。此值不会为 null。通常来自于创建
/// [Scrollable] 的 [ScrollView] 或其他小部件,或者,如果没有提供,则来自环境中的
/// [ScrollConfiguration]。
/// * `context`:一个 [ScrollContext],用于与将拥有此 [ScrollPosition] 对象的对象进行通信
/// (通常是 [Scrollable] 本身)。
/// * `oldPosition`:如果这是为此 [Scrollable] 创建的第一个 [ScrollPosition],则为 null;
/// 否则,它将是上一个实例。当环境发生变化,需要重新创建 [ScrollPosition] 时使用。
/// 如果这是第一次创建 [ScrollPosition],则为 null。
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
定义了 createScrollPosition
方法,用于为一个 Scrollable
小部件创建一个 ScrollPosition
对象。方法的注释提供了详细的说明,描述了它的行为和参数。
-
子类重写 :子类可以通过重写此方法来自定义其控制的滚动位置类型。例如,
PageController
会重写此方法,以便为其使用的滚动视图提供一个特定的滚动位置子类。 -
默认实现 :如果没有重写,默认会返回一个
ScrollPositionWithSingleContext
实例,它是一个标准的滚动位置类型。 -
参数说明:
physics
:滚动物理学,控制如何响应用户的滚动交互。context
:用于传递上下文信息,通常是Scrollable
自身的上下文。oldPosition
:如果这是为同一个Scrollable
创建的新的滚动位置,oldPosition
是先前的滚动位置。
总结
createScrollPosition
是一个工厂方法,用来为 Scrollable
小部件创建适当的 ScrollPosition
,它允许子类根据需要定制滚动行为和状态。
TrackingScrollController
scss
/// 一个 [ScrollController],它的 [initialScrollOffset] 跟踪其最新更新的 [ScrollPosition]。
///
/// 这个类可以用于同步两个或更多懒加载的滚动视图的滚动偏移,它们共享一个 [TrackingScrollController]。
/// 它会跟踪最近更新的滚动位置,并将其作为 `initialScrollOffset` 返回。
///
/// {@tool snippet}
///
/// 在这个示例中,每个 [PageView] 页面都包含一个 [ListView],而所有三个 [ListView] 都共享同一个 [TrackingScrollController]。
/// 所有三个列表视图的滚动偏移将彼此同步,以尽可能考虑不同列表长度的差异。
///
/// ```dart
/// PageView(
/// children: <Widget>[
/// ListView(
/// controller: _trackingScrollController,
/// children: List<Widget>.generate(100, (int i) => Text('page 0 item $i')).toList(),
/// ),
/// ListView(
/// controller: _trackingScrollController,
/// children: List<Widget>.generate(200, (int i) => Text('page 1 item $i')).toList(),
/// ),
/// ListView(
/// controller: _trackingScrollController,
/// children: List<Widget>.generate(300, (int i) => Text('page 2 item $i')).toList(),
/// ),
/// ],
/// )
/// ```
/// {@end-tool}
///
/// 在这个示例中,`_trackingController` 会由构建小部件树的有状态小部件创建。
class TrackingScrollController extends ScrollController {
/// 创建一个滚动控制器,持续更新其 [initialScrollOffset] 以匹配最后接收到的滚动通知。
TrackingScrollController({
super.initialScrollOffset,
super.keepScrollOffset,
super.debugLabel,
super.onAttach,
super.onDetach,
});
final Map<ScrollPosition, VoidCallback> _positionToListener = <ScrollPosition, VoidCallback>{};
ScrollPosition? _lastUpdated;
double? _lastUpdatedOffset;
/// 最后更改的 [ScrollPosition]。如果没有附加的滚动位置,或者尚未发生任何滚动,
/// 或者最后更改的 [ScrollPosition] 已被移除,则返回 null。
ScrollPosition? get mostRecentlyUpdatedPosition => _lastUpdated;
/// 返回 [mostRecentlyUpdatedPosition] 的滚动偏移,或者如果它为 null,则返回构造函数中提供的初始滚动偏移。
///
/// 另请参见:
///
/// * [ScrollController.initialScrollOffset],这是被此方法重写的。
@override
double get initialScrollOffset => _lastUpdatedOffset ?? super.initialScrollOffset;
@override
void attach(ScrollPosition position) {
super.attach(position);
assert(!_positionToListener.containsKey(position));
_positionToListener[position] = () {
_lastUpdated = position;
_lastUpdatedOffset = position.pixels;
};
position.addListener(_positionToListener[position]!);
}
@override
void detach(ScrollPosition position) {
super.detach(position);
assert(_positionToListener.containsKey(position));
position.removeListener(_positionToListener[position]!);
_positionToListener.remove(position);
if (_lastUpdated == position) {
_lastUpdated = null;
}
if (_positionToListener.isEmpty) {
_lastUpdatedOffset = null;
}
}
@override
void dispose() {
for (final ScrollPosition position in positions) {
assert(_positionToListener.containsKey(position));
position.removeListener(_positionToListener[position]!);
}
super.dispose();
}
}
TrackingScrollController
是 ScrollController
的一个子类,旨在使多个滚动视图的滚动偏移保持同步。它可以用于共享一个控制器的多个视图,尤其适用于需要多个视图滚动时保持一致的情况。此类通过跟踪最上次更新的 ScrollPosition
来更新 initialScrollOffset
。
关键功能
- 滚动同步 :通过共享一个
TrackingScrollController
,多个滚动视图的偏移量将保持同步,确保它们的滚动状态相互一致。 - 追踪滚动位置 :
_lastUpdated
变量用于存储最后一次更新的ScrollPosition
,_lastUpdatedOffset
则存储该位置的滚动偏移。 - attach / detach :在
attach
方法中,每次添加新的ScrollPosition
时,会为其设置监听器,更新_lastUpdated
和_lastUpdatedOffset
。在detach
方法中,会移除监听器,并在没有剩余滚动位置时重置initialScrollOffset
。 - 生命周期管理 :在
dispose
方法中,移除所有已附加位置的监听器,确保资源得到清理。
示例用法
在示例中,TrackingScrollController
被用来同步多个 ListView
的滚动位置。每个 PageView
页面都包含一个 ListView
,而所有这些 ListView
共享同一个控制器。这样,用户滚动其中一个列表时,其他列表的滚动偏移也会同步更新。