1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6///
7/// @docImport 'page_storage.dart';
8/// @docImport 'page_view.dart';
9/// @docImport 'scroll_metrics.dart';
10/// @docImport 'scroll_notification.dart';
11/// @docImport 'scroll_view.dart';
12/// @docImport 'single_child_scroll_view.dart';
13/// @docImport 'two_dimensional_scroll_view.dart';
14/// @docImport 'two_dimensional_viewport.dart';
15library;
16
17import 'dart:async';
18import 'dart:math' as math;
19
20import 'package:flutter/foundation.dart';
21import 'package:flutter/gestures.dart';
22import 'package:flutter/rendering.dart';
23import 'package:flutter/scheduler.dart';
24import 'package:flutter/services.dart';
25
26import 'basic.dart';
27import 'framework.dart';
28import 'gesture_detector.dart';
29import 'media_query.dart';
30import 'notification_listener.dart';
31import 'restoration.dart';
32import 'restoration_properties.dart';
33import 'scroll_activity.dart';
34import 'scroll_configuration.dart';
35import 'scroll_context.dart';
36import 'scroll_controller.dart';
37import 'scroll_physics.dart';
38import 'scroll_position.dart';
39import 'scrollable_helpers.dart';
40import 'selectable_region.dart';
41import 'selection_container.dart';
42import 'ticker_provider.dart';
43import 'view.dart';
44import 'viewport.dart';
45
46export 'package:flutter/physics.dart' show Tolerance;
47
48// Examples can assume:
49// late BuildContext context;
50
51/// Signature used by [Scrollable] to build the viewport through which the
52/// scrollable content is displayed.
53typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
54
55/// Signature used by [TwoDimensionalScrollable] to build the viewport through
56/// which the scrollable content is displayed.
57typedef TwoDimensionalViewportBuilder =
58 Widget Function(
59 BuildContext context,
60 ViewportOffset verticalPosition,
61 ViewportOffset horizontalPosition,
62 );
63
64// The return type of _performEnsureVisible.
65//
66// The list of futures represents each pending ScrollPosition call to
67// ensureVisible. The returned ScrollableState's context is used to find the
68// next potential ancestor Scrollable.
69typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
70
71/// A widget that manages scrolling in one dimension and informs the [Viewport]
72/// through which the content is viewed.
73///
74/// [Scrollable] implements the interaction model for a scrollable widget,
75/// including gesture recognition, but does not have an opinion about how the
76/// viewport, which actually displays the children, is constructed.
77///
78/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
79/// or [GridView], which combine scrolling, viewporting, and a layout model. To
80/// combine layout models (or to use a custom layout mode), consider using
81/// [CustomScrollView].
82///
83/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
84/// often used to interact with the [Scrollable] widget inside a [ListView] or
85/// a [GridView].
86///
87/// To further customize scrolling behavior with a [Scrollable]:
88///
89/// 1. You can provide a [viewportBuilder] to customize the child model. For
90/// example, [SingleChildScrollView] uses a viewport that displays a single
91/// box child whereas [CustomScrollView] uses a [Viewport] or a
92/// [ShrinkWrappingViewport], both of which display a list of slivers.
93///
94/// 2. You can provide a custom [ScrollController] that creates a custom
95/// [ScrollPosition] subclass. For example, [PageView] uses a
96/// [PageController], which creates a page-oriented scroll position subclass
97/// that keeps the same page visible when the [Scrollable] resizes.
98///
99/// ## Persisting the scroll position during a session
100///
101/// Scrollables attempt to persist their scroll position using [PageStorage].
102/// This can be disabled by setting [ScrollController.keepScrollOffset] to false
103/// on the [controller]. If it is enabled, using a [PageStorageKey] for the
104/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is
105/// recommended to help disambiguate different [Scrollable]s from each other.
106///
107/// See also:
108///
109/// * [ListView], which is a commonly used [ScrollView] that displays a
110/// scrolling, linear list of child widgets.
111/// * [PageView], which is a scrolling list of child widgets that are each the
112/// size of the viewport.
113/// * [GridView], which is a [ScrollView] that displays a scrolling, 2D array
114/// of child widgets.
115/// * [CustomScrollView], which is a [ScrollView] that creates custom scroll
116/// effects using slivers.
117/// * [SingleChildScrollView], which is a scrollable widget that has a single
118/// child.
119/// * [ScrollNotification] and [NotificationListener], which can be used to watch
120/// the scroll position without using a [ScrollController].
121class Scrollable extends StatefulWidget {
122 /// Creates a widget that scrolls.
123 const Scrollable({
124 super.key,
125 this.axisDirection = AxisDirection.down,
126 this.controller,
127 this.physics,
128 required this.viewportBuilder,
129 this.incrementCalculator,
130 this.excludeFromSemantics = false,
131 this.semanticChildCount,
132 this.dragStartBehavior = DragStartBehavior.start,
133 this.restorationId,
134 this.scrollBehavior,
135 this.clipBehavior = Clip.hardEdge,
136 this.hitTestBehavior = HitTestBehavior.opaque,
137 }) : assert(semanticChildCount == null || semanticChildCount >= 0);
138
139 /// {@template flutter.widgets.Scrollable.axisDirection}
140 /// The direction in which this widget scrolls.
141 ///
142 /// For example, if the [Scrollable.axisDirection] is [AxisDirection.down],
143 /// increasing the scroll position will cause content below the bottom of the
144 /// viewport to become visible through the viewport. Similarly, if the
145 /// axisDirection is [AxisDirection.right], increasing the scroll position
146 /// will cause content beyond the right edge of the viewport to become visible
147 /// through the viewport.
148 ///
149 /// Defaults to [AxisDirection.down].
150 /// {@endtemplate}
151 final AxisDirection axisDirection;
152
153 /// {@template flutter.widgets.Scrollable.controller}
154 /// An object that can be used to control the position to which this widget is
155 /// scrolled.
156 ///
157 /// A [ScrollController] serves several purposes. It can be used to control
158 /// the initial scroll position (see [ScrollController.initialScrollOffset]).
159 /// It can be used to control whether the scroll view should automatically
160 /// save and restore its scroll position in the [PageStorage] (see
161 /// [ScrollController.keepScrollOffset]). It can be used to read the current
162 /// scroll position (see [ScrollController.offset]), or change it (see
163 /// [ScrollController.animateTo]).
164 ///
165 /// If null, a [ScrollController] will be created internally by [Scrollable]
166 /// in order to create and manage the [ScrollPosition].
167 ///
168 /// See also:
169 ///
170 /// * [Scrollable.ensureVisible], which animates the scroll position to
171 /// reveal a given [BuildContext].
172 /// {@endtemplate}
173 final ScrollController? controller;
174
175 /// {@template flutter.widgets.Scrollable.physics}
176 /// How the widgets should respond to user input.
177 ///
178 /// For example, determines how the widget continues to animate after the
179 /// user stops dragging the scroll view.
180 ///
181 /// Defaults to matching platform conventions via the physics provided from
182 /// the ambient [ScrollConfiguration].
183 ///
184 /// If an explicit [ScrollBehavior] is provided to
185 /// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior
186 /// will take precedence after [Scrollable.physics].
187 ///
188 /// The physics can be changed dynamically, but new physics will only take
189 /// effect if the _class_ of the provided object changes. Merely constructing
190 /// a new instance with a different configuration is insufficient to cause the
191 /// physics to be reapplied. (This is because the final object used is
192 /// generated dynamically, which can be relatively expensive, and it would be
193 /// inefficient to speculatively create this object each frame to see if the
194 /// physics should be updated.)
195 ///
196 /// See also:
197 ///
198 /// * [AlwaysScrollableScrollPhysics], which can be used to indicate that the
199 /// scrollable should react to scroll requests (and possible overscroll)
200 /// even if the scrollable's contents fit without scrolling being necessary.
201 /// {@endtemplate}
202 final ScrollPhysics? physics;
203
204 /// Builds the viewport through which the scrollable content is displayed.
205 ///
206 /// A typical viewport uses the given [ViewportOffset] to determine which part
207 /// of its content is actually visible through the viewport.
208 ///
209 /// See also:
210 ///
211 /// * [Viewport], which is a viewport that displays a list of slivers.
212 /// * [ShrinkWrappingViewport], which is a viewport that displays a list of
213 /// slivers and sizes itself based on the size of the slivers.
214 final ViewportBuilder viewportBuilder;
215
216 /// {@template flutter.widgets.Scrollable.incrementCalculator}
217 /// An optional function that will be called to calculate the distance to
218 /// scroll when the scrollable is asked to scroll via the keyboard using a
219 /// [ScrollAction].
220 ///
221 /// If not supplied, the [Scrollable] will scroll a default amount when a
222 /// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow,
223 /// etc.), or otherwise invoked by a [ScrollAction].
224 ///
225 /// If [incrementCalculator] is null, the default for
226 /// [ScrollIncrementType.page] is 80% of the size of the scroll window, and
227 /// for [ScrollIncrementType.line], 50 logical pixels.
228 /// {@endtemplate}
229 final ScrollIncrementCalculator? incrementCalculator;
230
231 /// {@template flutter.widgets.scrollable.excludeFromSemantics}
232 /// Whether the scroll actions introduced by this [Scrollable] are exposed
233 /// in the semantics tree.
234 ///
235 /// Text fields with an overflow are usually scrollable to make sure that the
236 /// user can get to the beginning/end of the entered text. However, these
237 /// scrolling actions are generally not exposed to the semantics layer.
238 /// {@endtemplate}
239 ///
240 /// See also:
241 ///
242 /// * [GestureDetector.excludeFromSemantics], which is used to accomplish the
243 /// exclusion.
244 final bool excludeFromSemantics;
245
246 /// {@template flutter.widgets.scrollable.hitTestBehavior}
247 /// Defines the behavior of gesture detector used in this [Scrollable].
248 ///
249 /// This defaults to [HitTestBehavior.opaque] which means it prevents targets
250 /// behind this [Scrollable] from receiving events.
251 /// {@endtemplate}
252 ///
253 /// See also:
254 ///
255 /// * [HitTestBehavior], for an explanation on different behaviors.
256 final HitTestBehavior hitTestBehavior;
257
258 /// The number of children that will contribute semantic information.
259 ///
260 /// The value will be null if the number of children is unknown or unbounded.
261 ///
262 /// Some subtypes of [ScrollView] can infer this value automatically. For
263 /// example [ListView] will use the number of widgets in the child list,
264 /// while the [ListView.separated] constructor will use half that amount.
265 ///
266 /// For [CustomScrollView] and other types which do not receive a builder
267 /// or list of widgets, the child count must be explicitly provided.
268 ///
269 /// See also:
270 ///
271 /// * [CustomScrollView], for an explanation of scroll semantics.
272 /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property.
273 final int? semanticChildCount;
274
275 // TODO(jslavitz): Set the DragStartBehavior default to be start across all widgets.
276 /// {@template flutter.widgets.scrollable.dragStartBehavior}
277 /// Determines the way that drag start behavior is handled.
278 ///
279 /// If set to [DragStartBehavior.start], scrolling drag behavior will
280 /// begin at the position where the drag gesture won the arena. If set to
281 /// [DragStartBehavior.down] it will begin at the position where a down
282 /// event is first detected.
283 ///
284 /// In general, setting this to [DragStartBehavior.start] will make drag
285 /// animation smoother and setting it to [DragStartBehavior.down] will make
286 /// drag behavior feel slightly more reactive.
287 ///
288 /// By default, the drag start behavior is [DragStartBehavior.start].
289 ///
290 /// See also:
291 ///
292 /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
293 /// the different behaviors.
294 ///
295 /// {@endtemplate}
296 final DragStartBehavior dragStartBehavior;
297
298 /// {@template flutter.widgets.scrollable.restorationId}
299 /// Restoration ID to save and restore the scroll offset of the scrollable.
300 ///
301 /// If a restoration id is provided, the scrollable will persist its current
302 /// scroll offset and restore it during state restoration.
303 ///
304 /// The scroll offset is persisted in a [RestorationBucket] claimed from
305 /// the surrounding [RestorationScope] using the provided restoration ID.
306 ///
307 /// See also:
308 ///
309 /// * [RestorationManager], which explains how state restoration works in
310 /// Flutter.
311 /// {@endtemplate}
312 final String? restorationId;
313
314 /// {@template flutter.widgets.scrollable.scrollBehavior}
315 /// A [ScrollBehavior] that will be applied to this widget individually.
316 ///
317 /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
318 /// modified to alter the viewport decoration, like [Scrollbar]s.
319 ///
320 /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
321 /// [ScrollPhysics] is provided in [physics], it will take precedence,
322 /// followed by [scrollBehavior], and then the inherited ancestor
323 /// [ScrollBehavior].
324 /// {@endtemplate}
325 final ScrollBehavior? scrollBehavior;
326
327 /// {@macro flutter.material.Material.clipBehavior}
328 ///
329 /// Defaults to [Clip.hardEdge].
330 ///
331 /// This is passed to decorators in [ScrollableDetails], and does not directly affect
332 /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided
333 /// to [ScrollView.clipBehavior] and is supplied to the [Viewport].
334 final Clip clipBehavior;
335
336 /// The axis along which the scroll view scrolls.
337 ///
338 /// Determined by the [axisDirection].
339 Axis get axis => axisDirectionToAxis(axisDirection);
340
341 @override
342 ScrollableState createState() => ScrollableState();
343
344 @override
345 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
346 super.debugFillProperties(properties);
347 properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
348 properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics));
349 properties.add(StringProperty('restorationId', restorationId));
350 }
351
352 /// The state from the closest instance of this class that encloses the given
353 /// context, or null if none is found.
354 ///
355 /// Typical usage is as follows:
356 ///
357 /// ```dart
358 /// ScrollableState? scrollable = Scrollable.maybeOf(context);
359 /// ```
360 ///
361 /// Calling this method will create a dependency on the [ScrollableState]
362 /// that is returned, if there is one. This is typically the closest
363 /// [Scrollable], but may be a more distant ancestor if [axis] is used to
364 /// target a specific [Scrollable].
365 ///
366 /// Using the optional [Axis] is useful when Scrollables are nested and the
367 /// target [Scrollable] is not the closest instance. When [axis] is provided,
368 /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or
369 /// null if there is none.
370 ///
371 /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
372 /// means that if the `context` is that of a [Scrollable], it will _not_ find
373 /// _that_ [Scrollable].
374 ///
375 /// See also:
376 ///
377 /// * [Scrollable.of], which is similar to this method, but asserts
378 /// if no [Scrollable] ancestor is found.
379 static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) {
380 // This is the context that will need to establish the dependency.
381 final BuildContext originalContext = context;
382 InheritedElement? element = context.getElementForInheritedWidgetOfExactType<_ScrollableScope>();
383 while (element != null) {
384 final ScrollableState scrollable = (element.widget as _ScrollableScope).scrollable;
385 if (axis == null || axisDirectionToAxis(scrollable.axisDirection) == axis) {
386 // Establish the dependency on the correct context.
387 originalContext.dependOnInheritedElement(element);
388 return scrollable;
389 }
390 context = scrollable.context;
391 element = context.getElementForInheritedWidgetOfExactType<_ScrollableScope>();
392 }
393 return null;
394 }
395
396 /// The state from the closest instance of this class that encloses the given
397 /// context.
398 ///
399 /// Typical usage is as follows:
400 ///
401 /// ```dart
402 /// ScrollableState scrollable = Scrollable.of(context);
403 /// ```
404 ///
405 /// Calling this method will create a dependency on the [ScrollableState]
406 /// that is returned, if there is one. This is typically the closest
407 /// [Scrollable], but may be a more distant ancestor if [axis] is used to
408 /// target a specific [Scrollable].
409 ///
410 /// Using the optional [Axis] is useful when Scrollables are nested and the
411 /// target [Scrollable] is not the closest instance. When [axis] is provided,
412 /// the nearest enclosing [ScrollableState] in that [Axis] is returned.
413 ///
414 /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This
415 /// means that if the `context` is that of a [Scrollable], it will _not_ find
416 /// _that_ [Scrollable].
417 ///
418 /// If no [Scrollable] ancestor is found, then this method will assert in
419 /// debug mode, and throw an exception in release mode.
420 ///
421 /// See also:
422 ///
423 /// * [Scrollable.maybeOf], which is similar to this method, but returns null
424 /// if no [Scrollable] ancestor is found.
425 static ScrollableState of(BuildContext context, {Axis? axis}) {
426 final ScrollableState? scrollableState = maybeOf(context, axis: axis);
427 assert(() {
428 if (scrollableState == null) {
429 throw FlutterError.fromParts(<DiagnosticsNode>[
430 ErrorSummary(
431 'Scrollable.of() was called with a context that does not contain a '
432 'Scrollable widget.',
433 ),
434 ErrorDescription(
435 'No Scrollable widget ancestor could be found '
436 '${axis == null ? '' : 'for the provided Axis: $axis '}'
437 'starting from the context that was passed to Scrollable.of(). This '
438 'can happen because you are using a widget that looks for a Scrollable '
439 'ancestor, but no such ancestor exists.\n'
440 'The context used was:\n'
441 ' $context',
442 ),
443 if (axis != null)
444 ErrorHint(
445 'When specifying an axis, this method will only look for a Scrollable '
446 'that matches the given Axis.',
447 ),
448 ]);
449 }
450 return true;
451 }());
452 return scrollableState!;
453 }
454
455 /// Provides a heuristic to determine if expensive frame-bound tasks should be
456 /// deferred for the [context] at a specific point in time.
457 ///
458 /// Calling this method does _not_ create a dependency on any other widget.
459 /// This also means that the value returned is only good for the point in time
460 /// when it is called, and callers will not get updated if the value changes.
461 ///
462 /// The heuristic used is determined by the [physics] of this [Scrollable]
463 /// via [ScrollPhysics.recommendDeferredLoading]. That method is called with
464 /// the current [ScrollPosition.activity]'s [ScrollActivity.velocity].
465 ///
466 /// The optional [Axis] allows targeting of a specific [Scrollable] of that
467 /// axis, useful when Scrollables are nested. When [axis] is provided,
468 /// [ScrollPosition.recommendDeferredLoading] is called for the nearest
469 /// [Scrollable] in that [Axis].
470 ///
471 /// If there is no [Scrollable] in the widget tree above the [context], this
472 /// method returns false.
473 static bool recommendDeferredLoadingForContext(BuildContext context, {Axis? axis}) {
474 _ScrollableScope? widget = context.getInheritedWidgetOfExactType<_ScrollableScope>();
475 while (widget != null) {
476 if (axis == null || axisDirectionToAxis(widget.scrollable.axisDirection) == axis) {
477 return widget.position.recommendDeferredLoading(context);
478 }
479 context = widget.scrollable.context;
480 widget = context.getInheritedWidgetOfExactType<_ScrollableScope>();
481 }
482 return false;
483 }
484
485 /// Scrolls all scrollables that enclose the given context so as to make the
486 /// given context visible.
487 ///
488 /// If a [Scrollable] enclosing the provided [BuildContext] is a
489 /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure
490 /// the target is made visible.
491 static Future<void> ensureVisible(
492 BuildContext context, {
493 double alignment = 0.0,
494 Duration duration = Duration.zero,
495 Curve curve = Curves.ease,
496 ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
497 }) {
498 final List<Future<void>> futures = <Future<void>>[];
499
500 // The targetRenderObject is used to record the first target renderObject.
501 // If there are multiple scrollable widgets nested, the targetRenderObject
502 // is made to be as visible as possible to improve the user experience. If
503 // the targetRenderObject is already visible, then let the outer
504 // renderObject be as visible as possible.
505 //
506 // Also see https://github.com/flutter/flutter/issues/65100
507 RenderObject? targetRenderObject;
508 ScrollableState? scrollable = Scrollable.maybeOf(context);
509 while (scrollable != null) {
510 final List<Future<void>> newFutures;
511 (newFutures, scrollable) = scrollable._performEnsureVisible(
512 context.findRenderObject()!,
513 alignment: alignment,
514 duration: duration,
515 curve: curve,
516 alignmentPolicy: alignmentPolicy,
517 targetRenderObject: targetRenderObject,
518 );
519 futures.addAll(newFutures);
520
521 targetRenderObject ??= context.findRenderObject();
522 context = scrollable.context;
523 scrollable = Scrollable.maybeOf(context);
524 }
525
526 if (futures.isEmpty || duration == Duration.zero) {
527 return Future<void>.value();
528 }
529 if (futures.length == 1) {
530 return futures.single;
531 }
532 return Future.wait<void>(futures).then<void>((List<void> _) => null);
533 }
534}
535
536// Enable Scrollable.of() to work as if ScrollableState was an inherited widget.
537// ScrollableState.build() always rebuilds its _ScrollableScope.
538class _ScrollableScope extends InheritedWidget {
539 const _ScrollableScope({required this.scrollable, required this.position, required super.child});
540
541 final ScrollableState scrollable;
542 final ScrollPosition position;
543
544 @override
545 bool updateShouldNotify(_ScrollableScope old) {
546 return position != old.position;
547 }
548}
549
550/// State object for a [Scrollable] widget.
551///
552/// To manipulate a [Scrollable] widget's scroll position, use the object
553/// obtained from the [position] property.
554///
555/// To be informed of when a [Scrollable] widget is scrolling, use a
556/// [NotificationListener] to listen for [ScrollNotification] notifications.
557///
558/// This class is not intended to be subclassed. To specialize the behavior of a
559/// [Scrollable], provide it with a [ScrollPhysics].
560class ScrollableState extends State<Scrollable>
561 with TickerProviderStateMixin, RestorationMixin
562 implements ScrollContext {
563 // GETTERS
564
565 /// The manager for this [Scrollable] widget's viewport position.
566 ///
567 /// To control what kind of [ScrollPosition] is created for a [Scrollable],
568 /// provide it with custom [ScrollController] that creates the appropriate
569 /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
570 ScrollPosition get position => _position!;
571 ScrollPosition? _position;
572
573 /// The resolved [ScrollPhysics] of the [ScrollableState].
574 ScrollPhysics? get resolvedPhysics => _physics;
575 ScrollPhysics? _physics;
576
577 /// An [Offset] that represents the absolute distance from the origin, or 0,
578 /// of the [ScrollPosition] expressed in the associated [Axis].
579 ///
580 /// Used by [EdgeDraggingAutoScroller] to progress the position forward when a
581 /// drag gesture reaches the edge of the [Viewport].
582 Offset get deltaToScrollOrigin => switch (axisDirection) {
583 AxisDirection.up => Offset(0, -position.pixels),
584 AxisDirection.down => Offset(0, position.pixels),
585 AxisDirection.left => Offset(-position.pixels, 0),
586 AxisDirection.right => Offset(position.pixels, 0),
587 };
588
589 ScrollController get _effectiveScrollController =>
590 widget.controller ?? _fallbackScrollController!;
591
592 @override
593 AxisDirection get axisDirection => widget.axisDirection;
594
595 @override
596 TickerProvider get vsync => this;
597
598 @override
599 double get devicePixelRatio => _devicePixelRatio;
600 late double _devicePixelRatio;
601
602 @override
603 BuildContext? get notificationContext => _gestureDetectorKey.currentContext;
604
605 @override
606 BuildContext get storageContext => context;
607
608 @override
609 String? get restorationId => widget.restorationId;
610 final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset();
611
612 late ScrollBehavior _configuration;
613 ScrollController? _fallbackScrollController;
614 DeviceGestureSettings? _mediaQueryGestureSettings;
615
616 // Only call this from places that will definitely trigger a rebuild.
617 void _updatePosition() {
618 _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
619 final ScrollPhysics? physicsFromWidget =
620 widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context);
621 _physics = _configuration.getScrollPhysics(context);
622 _physics = physicsFromWidget?.applyTo(_physics) ?? _physics;
623
624 final ScrollPosition? oldPosition = _position;
625 if (oldPosition != null) {
626 _effectiveScrollController.detach(oldPosition);
627 // It's important that we not dispose the old position until after the
628 // viewport has had a chance to unregister its listeners from the old
629 // position. So, schedule a microtask to do it.
630 scheduleMicrotask(oldPosition.dispose);
631 }
632
633 _position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPosition);
634 assert(_position != null);
635 _effectiveScrollController.attach(position);
636 }
637
638 @protected
639 @override
640 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
641 registerForRestoration(_persistedScrollOffset, 'offset');
642 assert(_position != null);
643 if (_persistedScrollOffset.value != null) {
644 position.restoreOffset(_persistedScrollOffset.value!, initialRestore: initialRestore);
645 }
646 }
647
648 @protected
649 @override
650 void saveOffset(double offset) {
651 assert(debugIsSerializableForRestoration(offset));
652 _persistedScrollOffset.value = offset;
653 // [saveOffset] is called after a scrolling ends and it is usually not
654 // followed by a frame. Therefore, manually flush restoration data.
655 ServicesBinding.instance.restorationManager.flushData();
656 }
657
658 @protected
659 @override
660 void initState() {
661 if (widget.controller == null) {
662 _fallbackScrollController = ScrollController();
663 }
664 super.initState();
665 }
666
667 @protected
668 @override
669 void didChangeDependencies() {
670 _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context);
671 _devicePixelRatio =
672 MediaQuery.maybeDevicePixelRatioOf(context) ?? View.of(context).devicePixelRatio;
673 _updatePosition();
674 super.didChangeDependencies();
675 }
676
677 bool _shouldUpdatePosition(Scrollable oldWidget) {
678 if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) {
679 return true;
680 }
681 if (widget.scrollBehavior != null &&
682 oldWidget.scrollBehavior != null &&
683 widget.scrollBehavior!.shouldNotify(oldWidget.scrollBehavior!)) {
684 return true;
685 }
686 ScrollPhysics? newPhysics = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context);
687 ScrollPhysics? oldPhysics =
688 oldWidget.physics ?? oldWidget.scrollBehavior?.getScrollPhysics(context);
689 do {
690 if (newPhysics?.runtimeType != oldPhysics?.runtimeType) {
691 return true;
692 }
693 newPhysics = newPhysics?.parent;
694 oldPhysics = oldPhysics?.parent;
695 } while (newPhysics != null || oldPhysics != null);
696
697 return widget.controller?.runtimeType != oldWidget.controller?.runtimeType;
698 }
699
700 @protected
701 @override
702 void didUpdateWidget(Scrollable oldWidget) {
703 super.didUpdateWidget(oldWidget);
704
705 if (widget.controller != oldWidget.controller) {
706 if (oldWidget.controller == null) {
707 // The old controller was null, meaning the fallback cannot be null.
708 // Dispose of the fallback.
709 assert(_fallbackScrollController != null);
710 assert(widget.controller != null);
711 _fallbackScrollController!.detach(position);
712 _fallbackScrollController!.dispose();
713 _fallbackScrollController = null;
714 } else {
715 // The old controller was not null, detach.
716 oldWidget.controller?.detach(position);
717 if (widget.controller == null) {
718 // If the new controller is null, we need to set up the fallback
719 // ScrollController.
720 _fallbackScrollController = ScrollController();
721 }
722 }
723 // Attach the updated effective scroll controller.
724 _effectiveScrollController.attach(position);
725 }
726
727 if (_shouldUpdatePosition(oldWidget)) {
728 _updatePosition();
729 }
730 }
731
732 @protected
733 @override
734 void dispose() {
735 if (widget.controller != null) {
736 widget.controller!.detach(position);
737 } else {
738 _fallbackScrollController?.detach(position);
739 _fallbackScrollController?.dispose();
740 }
741
742 position.dispose();
743 _persistedScrollOffset.dispose();
744 super.dispose();
745 }
746
747 // SEMANTICS
748
749 final GlobalKey _scrollSemanticsKey = GlobalKey();
750
751 @override
752 @protected
753 void setSemanticsActions(Set<SemanticsAction> actions) {
754 if (_gestureDetectorKey.currentState != null) {
755 _gestureDetectorKey.currentState!.replaceSemanticsActions(actions);
756 }
757 }
758
759 // GESTURE RECOGNITION AND POINTER IGNORING
760
761 final GlobalKey<RawGestureDetectorState> _gestureDetectorKey =
762 GlobalKey<RawGestureDetectorState>();
763 final GlobalKey _ignorePointerKey = GlobalKey();
764
765 // This field is set during layout, and then reused until the next time it is set.
766 Map<Type, GestureRecognizerFactory> _gestureRecognizers =
767 const <Type, GestureRecognizerFactory>{};
768 bool _shouldIgnorePointer = false;
769
770 bool? _lastCanDrag;
771 Axis? _lastAxisDirection;
772
773 @override
774 @protected
775 void setCanDrag(bool value) {
776 if (value == _lastCanDrag && (!value || widget.axis == _lastAxisDirection)) {
777 return;
778 }
779 if (!value) {
780 _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
781 // Cancel the active hold/drag (if any) because the gesture recognizers
782 // will soon be disposed by our RawGestureDetector, and we won't be
783 // receiving pointer up events to cancel the hold/drag.
784 _handleDragCancel();
785 } else {
786 switch (widget.axis) {
787 case Axis.vertical:
788 _gestureRecognizers = <Type, GestureRecognizerFactory>{
789 VerticalDragGestureRecognizer:
790 GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
791 () => VerticalDragGestureRecognizer(supportedDevices: _configuration.dragDevices),
792 (VerticalDragGestureRecognizer instance) {
793 instance
794 ..onDown = _handleDragDown
795 ..onStart = _handleDragStart
796 ..onUpdate = _handleDragUpdate
797 ..onEnd = _handleDragEnd
798 ..onCancel = _handleDragCancel
799 ..minFlingDistance = _physics?.minFlingDistance
800 ..minFlingVelocity = _physics?.minFlingVelocity
801 ..maxFlingVelocity = _physics?.maxFlingVelocity
802 ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
803 ..dragStartBehavior = widget.dragStartBehavior
804 ..multitouchDragStrategy = _configuration.getMultitouchDragStrategy(context)
805 ..gestureSettings = _mediaQueryGestureSettings
806 ..supportedDevices = _configuration.dragDevices;
807 },
808 ),
809 };
810 case Axis.horizontal:
811 _gestureRecognizers = <Type, GestureRecognizerFactory>{
812 HorizontalDragGestureRecognizer:
813 GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
814 () =>
815 HorizontalDragGestureRecognizer(supportedDevices: _configuration.dragDevices),
816 (HorizontalDragGestureRecognizer instance) {
817 instance
818 ..onDown = _handleDragDown
819 ..onStart = _handleDragStart
820 ..onUpdate = _handleDragUpdate
821 ..onEnd = _handleDragEnd
822 ..onCancel = _handleDragCancel
823 ..minFlingDistance = _physics?.minFlingDistance
824 ..minFlingVelocity = _physics?.minFlingVelocity
825 ..maxFlingVelocity = _physics?.maxFlingVelocity
826 ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
827 ..dragStartBehavior = widget.dragStartBehavior
828 ..multitouchDragStrategy = _configuration.getMultitouchDragStrategy(context)
829 ..gestureSettings = _mediaQueryGestureSettings
830 ..supportedDevices = _configuration.dragDevices;
831 },
832 ),
833 };
834 }
835 }
836 _lastCanDrag = value;
837 _lastAxisDirection = widget.axis;
838 if (_gestureDetectorKey.currentState != null) {
839 _gestureDetectorKey.currentState!.replaceGestureRecognizers(_gestureRecognizers);
840 }
841 }
842
843 @override
844 @protected
845 void setIgnorePointer(bool value) {
846 if (_shouldIgnorePointer == value) {
847 return;
848 }
849 _shouldIgnorePointer = value;
850 if (_ignorePointerKey.currentContext != null) {
851 final RenderIgnorePointer renderBox =
852 _ignorePointerKey.currentContext!.findRenderObject()! as RenderIgnorePointer;
853 renderBox.ignoring = _shouldIgnorePointer;
854 }
855 }
856
857 // TOUCH HANDLERS
858
859 Drag? _drag;
860 ScrollHoldController? _hold;
861
862 void _handleDragDown(DragDownDetails details) {
863 assert(_drag == null);
864 assert(_hold == null);
865 _hold = position.hold(_disposeHold);
866 }
867
868 void _handleDragStart(DragStartDetails details) {
869 // It's possible for _hold to become null between _handleDragDown and
870 // _handleDragStart, for example if some user code calls jumpTo or otherwise
871 // triggers a new activity to begin.
872 assert(_drag == null);
873 _drag = position.drag(details, _disposeDrag);
874 assert(_drag != null);
875 // _hold might be non-null if the scroll position is currently animating.
876 if (_hold != null) {
877 _disposeHold();
878 }
879 }
880
881 void _handleDragUpdate(DragUpdateDetails details) {
882 // _drag might be null if the drag activity ended and called _disposeDrag.
883 assert(_hold == null || _drag == null);
884 _drag?.update(details);
885 }
886
887 void _handleDragEnd(DragEndDetails details) {
888 // _drag might be null if the drag activity ended and called _disposeDrag.
889 assert(_hold == null || _drag == null);
890 _drag?.end(details);
891 assert(_drag == null);
892 }
893
894 void _handleDragCancel() {
895 if (_gestureDetectorKey.currentContext == null) {
896 // The cancel was caused by the GestureDetector getting disposed, which
897 // means we will get disposed momentarily as well and shouldn't do
898 // any work.
899 return;
900 }
901 // _hold might be null if the drag started.
902 // _drag might be null if the drag activity ended and called _disposeDrag.
903 assert(_hold == null || _drag == null);
904 _hold?.cancel();
905 _drag?.cancel();
906 assert(_hold == null);
907 assert(_drag == null);
908 }
909
910 void _disposeHold() {
911 _hold = null;
912 }
913
914 void _disposeDrag() {
915 _drag = null;
916 }
917
918 // SCROLL WHEEL
919
920 // Returns the offset that should result from applying [event] to the current
921 // position, taking min/max scroll extent into account.
922 double _targetScrollOffsetForPointerScroll(double delta) {
923 return math.min(
924 math.max(position.pixels + delta, position.minScrollExtent),
925 position.maxScrollExtent,
926 );
927 }
928
929 // Returns the delta that should result from applying [event] with axis,
930 // direction, and any modifiers specified by the ScrollBehavior taken into
931 // account.
932 double _pointerSignalEventDelta(PointerScrollEvent event) {
933 final Set<LogicalKeyboardKey> pressed = HardwareKeyboard.instance.logicalKeysPressed;
934 final bool flipAxes =
935 pressed.any(_configuration.pointerAxisModifiers.contains) &&
936 // Axes are only flipped for physical mouse wheel input.
937 // On some platforms, like web, trackpad input is handled through pointer
938 // signals, but should not be included in this axis modifying behavior.
939 // This is because on a trackpad, all directional axes are available to
940 // the user, while mouse scroll wheels typically are restricted to one
941 // axis.
942 event.kind == PointerDeviceKind.mouse;
943
944 final Axis axis = flipAxes ? flipAxis(widget.axis) : widget.axis;
945 final double delta = switch (axis) {
946 Axis.horizontal => event.scrollDelta.dx,
947 Axis.vertical => event.scrollDelta.dy,
948 };
949
950 return axisDirectionIsReversed(widget.axisDirection) ? -delta : delta;
951 }
952
953 void _receivedPointerSignal(PointerSignalEvent event) {
954 if (event is PointerScrollEvent && _position != null) {
955 if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
956 // The handler won't use the `event`, so allow the platform to trigger
957 // any default native actions.
958 event.respond(allowPlatformDefault: true);
959 return;
960 }
961 final double delta = _pointerSignalEventDelta(event);
962 final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
963 // Only express interest in the event if it would actually result in a scroll.
964 if (delta != 0.0 && targetScrollOffset != position.pixels) {
965 GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
966 return;
967 }
968 // The `event` won't result in a scroll, so allow the platform to trigger
969 // any default native actions.
970 event.respond(allowPlatformDefault: true);
971 } else if (event is PointerScrollInertiaCancelEvent) {
972 position.pointerScroll(0);
973 // Don't use the pointer signal resolver, all hit-tested scrollables should stop.
974 }
975 }
976
977 void _handlePointerScroll(PointerEvent event) {
978 assert(event is PointerScrollEvent);
979 final double delta = _pointerSignalEventDelta(event as PointerScrollEvent);
980 final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
981 if (delta != 0.0 && targetScrollOffset != position.pixels) {
982 position.pointerScroll(delta);
983 }
984 }
985
986 bool _handleScrollMetricsNotification(ScrollMetricsNotification notification) {
987 if (notification.depth == 0) {
988 final RenderObject? scrollSemanticsRenderObject =
989 _scrollSemanticsKey.currentContext?.findRenderObject();
990 if (scrollSemanticsRenderObject != null) {
991 scrollSemanticsRenderObject.markNeedsSemanticsUpdate();
992 }
993 }
994 return false;
995 }
996
997 Widget _buildChrome(BuildContext context, Widget child) {
998 final ScrollableDetails details = ScrollableDetails(
999 direction: widget.axisDirection,
1000 controller: _effectiveScrollController,
1001 decorationClipBehavior: widget.clipBehavior,
1002 );
1003
1004 return _configuration.buildScrollbar(
1005 context,
1006 _configuration.buildOverscrollIndicator(context, child, details),
1007 details,
1008 );
1009 }
1010
1011 // DESCRIPTION
1012
1013 @protected
1014 @override
1015 Widget build(BuildContext context) {
1016 assert(_position != null);
1017 // _ScrollableScope must be placed above the BuildContext returned by notificationContext
1018 // so that we can get this ScrollableState by doing the following:
1019 //
1020 // ScrollNotification notification;
1021 // Scrollable.of(notification.context)
1022 //
1023 // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope
1024 // must be placed above the widget using it: RawGestureDetector
1025 Widget result = _ScrollableScope(
1026 scrollable: this,
1027 position: position,
1028 child: Listener(
1029 onPointerSignal: _receivedPointerSignal,
1030 child: RawGestureDetector(
1031 key: _gestureDetectorKey,
1032 gestures: _gestureRecognizers,
1033 behavior: widget.hitTestBehavior,
1034 excludeFromSemantics: widget.excludeFromSemantics,
1035 child: Semantics(
1036 explicitChildNodes: !widget.excludeFromSemantics,
1037 child: IgnorePointer(
1038 key: _ignorePointerKey,
1039 ignoring: _shouldIgnorePointer,
1040 child: widget.viewportBuilder(context, position),
1041 ),
1042 ),
1043 ),
1044 ),
1045 );
1046
1047 if (!widget.excludeFromSemantics) {
1048 result = NotificationListener<ScrollMetricsNotification>(
1049 onNotification: _handleScrollMetricsNotification,
1050 child: _ScrollSemantics(
1051 key: _scrollSemanticsKey,
1052 position: position,
1053 allowImplicitScrolling: _physics!.allowImplicitScrolling,
1054 axis: widget.axis,
1055 semanticChildCount: widget.semanticChildCount,
1056 child: result,
1057 ),
1058 );
1059 }
1060
1061 result = _buildChrome(context, result);
1062
1063 // Selection is only enabled when there is a parent registrar.
1064 final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
1065 if (registrar != null) {
1066 result = _ScrollableSelectionHandler(
1067 state: this,
1068 position: position,
1069 registrar: registrar,
1070 child: result,
1071 );
1072 }
1073
1074 return result;
1075 }
1076
1077 // Returns the Future from calling ensureVisible for the ScrollPosition, as
1078 // as well as this ScrollableState instance so its context can be used to
1079 // check for other ancestor Scrollables in executing ensureVisible.
1080 _EnsureVisibleResults _performEnsureVisible(
1081 RenderObject object, {
1082 double alignment = 0.0,
1083 Duration duration = Duration.zero,
1084 Curve curve = Curves.ease,
1085 ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
1086 RenderObject? targetRenderObject,
1087 }) {
1088 final Future<void> ensureVisibleFuture = position.ensureVisible(
1089 object,
1090 alignment: alignment,
1091 duration: duration,
1092 curve: curve,
1093 alignmentPolicy: alignmentPolicy,
1094 targetRenderObject: targetRenderObject,
1095 );
1096 return (<Future<void>>[ensureVisibleFuture], this);
1097 }
1098
1099 @protected
1100 @override
1101 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1102 super.debugFillProperties(properties);
1103 properties.add(DiagnosticsProperty<ScrollPosition>('position', _position));
1104 properties.add(DiagnosticsProperty<ScrollPhysics>('effective physics', _physics));
1105 }
1106}
1107
1108/// A widget to handle selection for a scrollable.
1109///
1110/// This widget registers itself to the [registrar] and uses
1111/// [SelectionContainer] to collect selectables from its subtree.
1112class _ScrollableSelectionHandler extends StatefulWidget {
1113 const _ScrollableSelectionHandler({
1114 required this.state,
1115 required this.position,
1116 required this.registrar,
1117 required this.child,
1118 });
1119
1120 final ScrollableState state;
1121 final ScrollPosition position;
1122 final Widget child;
1123 final SelectionRegistrar registrar;
1124
1125 @override
1126 _ScrollableSelectionHandlerState createState() => _ScrollableSelectionHandlerState();
1127}
1128
1129class _ScrollableSelectionHandlerState extends State<_ScrollableSelectionHandler> {
1130 late _ScrollableSelectionContainerDelegate _selectionDelegate;
1131
1132 @override
1133 void initState() {
1134 super.initState();
1135 _selectionDelegate = _ScrollableSelectionContainerDelegate(
1136 state: widget.state,
1137 position: widget.position,
1138 );
1139 }
1140
1141 @override
1142 void didUpdateWidget(_ScrollableSelectionHandler oldWidget) {
1143 super.didUpdateWidget(oldWidget);
1144 if (oldWidget.position != widget.position) {
1145 _selectionDelegate.position = widget.position;
1146 }
1147 }
1148
1149 @override
1150 void dispose() {
1151 _selectionDelegate.dispose();
1152 super.dispose();
1153 }
1154
1155 @override
1156 Widget build(BuildContext context) {
1157 return SelectionContainer(
1158 registrar: widget.registrar,
1159 delegate: _selectionDelegate,
1160 child: widget.child,
1161 );
1162 }
1163}
1164
1165/// This updater handles the case where the selectables change frequently, and
1166/// it optimizes toward scrolling updates.
1167///
1168/// It keeps track of the drag start offset relative to scroll origin for every
1169/// selectable. The records are used to determine whether the selection is up to
1170/// date with the scroll position when it sends the drag update event to a
1171/// selectable.
1172class _ScrollableSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1173 _ScrollableSelectionContainerDelegate({required this.state, required ScrollPosition position})
1174 : _position = position,
1175 _autoScroller = EdgeDraggingAutoScroller(
1176 state,
1177 velocityScalar: _kDefaultSelectToScrollVelocityScalar,
1178 ) {
1179 _position.addListener(_scheduleLayoutChange);
1180 }
1181
1182 // Pointer drag is a single point, it should not have a size.
1183 static const double _kDefaultDragTargetSize = 0;
1184
1185 // An eye-balled value for a smooth scrolling speed.
1186 static const double _kDefaultSelectToScrollVelocityScalar = 30;
1187
1188 final ScrollableState state;
1189 final EdgeDraggingAutoScroller _autoScroller;
1190 bool _scheduledLayoutChange = false;
1191 Offset? _currentDragStartRelatedToOrigin;
1192 Offset? _currentDragEndRelatedToOrigin;
1193
1194 // The scrollable only auto scrolls if the selection starts in the scrollable.
1195 bool _selectionStartsInScrollable = false;
1196
1197 ScrollPosition get position => _position;
1198 ScrollPosition _position;
1199 set position(ScrollPosition other) {
1200 if (other == _position) {
1201 return;
1202 }
1203 _position.removeListener(_scheduleLayoutChange);
1204 _position = other;
1205 _position.addListener(_scheduleLayoutChange);
1206 }
1207
1208 // The layout will only be updated a frame later than position changes.
1209 // Schedule PostFrameCallback to capture the accurate layout.
1210 void _scheduleLayoutChange() {
1211 if (_scheduledLayoutChange) {
1212 return;
1213 }
1214 _scheduledLayoutChange = true;
1215 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
1216 if (!_scheduledLayoutChange) {
1217 return;
1218 }
1219 _scheduledLayoutChange = false;
1220 layoutDidChange();
1221 }, debugLabel: 'ScrollableSelectionContainer.layoutDidChange');
1222 }
1223
1224 /// Stores the scroll offset when a scrollable receives the last
1225 /// [SelectionEdgeUpdateEvent].
1226 ///
1227 /// The stored scroll offset may be null if a scrollable never receives a
1228 /// [SelectionEdgeUpdateEvent].
1229 ///
1230 /// When a new [SelectionEdgeUpdateEvent] is dispatched to a selectable, this
1231 /// updater checks the current scroll offset against the one stored in these
1232 /// records. If the scroll offset is different, it synthesizes an opposite
1233 /// [SelectionEdgeUpdateEvent] and dispatches the event before dispatching the
1234 /// new event.
1235 ///
1236 /// For example, if a selectable receives an end [SelectionEdgeUpdateEvent]
1237 /// and its scroll offset in the records is different from the current value,
1238 /// it synthesizes a start [SelectionEdgeUpdateEvent] and dispatches it before
1239 /// dispatching the original end [SelectionEdgeUpdateEvent].
1240 final Map<Selectable, double> _selectableStartEdgeUpdateRecords = <Selectable, double>{};
1241 final Map<Selectable, double> _selectableEndEdgeUpdateRecords = <Selectable, double>{};
1242
1243 @override
1244 void didChangeSelectables() {
1245 final Set<Selectable> selectableSet = selectables.toSet();
1246 _selectableStartEdgeUpdateRecords.removeWhere(
1247 (Selectable key, double value) => !selectableSet.contains(key),
1248 );
1249 _selectableEndEdgeUpdateRecords.removeWhere(
1250 (Selectable key, double value) => !selectableSet.contains(key),
1251 );
1252 super.didChangeSelectables();
1253 }
1254
1255 @override
1256 SelectionResult handleClearSelection(ClearSelectionEvent event) {
1257 _selectableStartEdgeUpdateRecords.clear();
1258 _selectableEndEdgeUpdateRecords.clear();
1259 _currentDragStartRelatedToOrigin = null;
1260 _currentDragEndRelatedToOrigin = null;
1261 _selectionStartsInScrollable = false;
1262 return super.handleClearSelection(event);
1263 }
1264
1265 @override
1266 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
1267 if (_currentDragEndRelatedToOrigin == null && _currentDragStartRelatedToOrigin == null) {
1268 assert(!_selectionStartsInScrollable);
1269 _selectionStartsInScrollable = _globalPositionInScrollable(event.globalPosition);
1270 }
1271 final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
1272 if (event.type == SelectionEventType.endEdgeUpdate) {
1273 _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
1274 final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(
1275 -deltaToOrigin.dx,
1276 -deltaToOrigin.dy,
1277 );
1278 event = SelectionEdgeUpdateEvent.forEnd(
1279 globalPosition: endOffset,
1280 granularity: event.granularity,
1281 );
1282 } else {
1283 _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin(event.globalPosition);
1284 final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(
1285 -deltaToOrigin.dx,
1286 -deltaToOrigin.dy,
1287 );
1288 event = SelectionEdgeUpdateEvent.forStart(
1289 globalPosition: startOffset,
1290 granularity: event.granularity,
1291 );
1292 }
1293 final SelectionResult result = super.handleSelectionEdgeUpdate(event);
1294
1295 // Result may be pending if one of the selectable child is also a scrollable.
1296 // In that case, the parent scrollable needs to wait for the child to finish
1297 // scrolling.
1298 if (result == SelectionResult.pending) {
1299 _autoScroller.stopAutoScroll();
1300 return result;
1301 }
1302 if (_selectionStartsInScrollable) {
1303 _autoScroller.startAutoScrollIfNecessary(_dragTargetFromEvent(event));
1304 if (_autoScroller.scrolling) {
1305 return SelectionResult.pending;
1306 }
1307 }
1308 return result;
1309 }
1310
1311 Offset _inferPositionRelatedToOrigin(Offset globalPosition) {
1312 final RenderBox box = state.context.findRenderObject()! as RenderBox;
1313 final Offset localPosition = box.globalToLocal(globalPosition);
1314 if (!_selectionStartsInScrollable) {
1315 // If the selection starts outside of the scrollable, selecting across the
1316 // scrollable boundary will act as selecting the entire content in the
1317 // scrollable. This logic move the offset to the 0.0 or infinity to cover
1318 // the entire content if the input position is outside of the scrollable.
1319 if (localPosition.dy < 0 || localPosition.dx < 0) {
1320 return box.localToGlobal(Offset.zero);
1321 }
1322 if (localPosition.dy > box.size.height || localPosition.dx > box.size.width) {
1323 return Offset.infinite;
1324 }
1325 }
1326 final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
1327 return box.localToGlobal(localPosition.translate(deltaToOrigin.dx, deltaToOrigin.dy));
1328 }
1329
1330 /// Infers the [_currentDragStartRelatedToOrigin] and
1331 /// [_currentDragEndRelatedToOrigin] from the geometry.
1332 ///
1333 /// This method is called after a select word and select all event where the
1334 /// selection is triggered by none drag events. The
1335 /// [_currentDragStartRelatedToOrigin] and [_currentDragEndRelatedToOrigin]
1336 /// are essential to handle future [SelectionEdgeUpdateEvent]s.
1337 void _updateDragLocationsFromGeometries({
1338 bool forceUpdateStart = true,
1339 bool forceUpdateEnd = true,
1340 }) {
1341 final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
1342 final RenderBox box = state.context.findRenderObject()! as RenderBox;
1343 final Matrix4 transform = box.getTransformTo(null);
1344 if (currentSelectionStartIndex != -1 &&
1345 (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) {
1346 final SelectionGeometry geometry = selectables[currentSelectionStartIndex].value;
1347 assert(geometry.hasSelection);
1348 final SelectionPoint start = geometry.startSelectionPoint!;
1349 final Matrix4 childTransform = selectables[currentSelectionStartIndex].getTransformTo(box);
1350 final Offset localDragStart = MatrixUtils.transformPoint(
1351 childTransform,
1352 start.localPosition + Offset(0, -start.lineHeight / 2),
1353 );
1354 _currentDragStartRelatedToOrigin = MatrixUtils.transformPoint(
1355 transform,
1356 localDragStart + deltaToOrigin,
1357 );
1358 }
1359 if (currentSelectionEndIndex != -1 &&
1360 (_currentDragEndRelatedToOrigin == null || forceUpdateEnd)) {
1361 final SelectionGeometry geometry = selectables[currentSelectionEndIndex].value;
1362 assert(geometry.hasSelection);
1363 final SelectionPoint end = geometry.endSelectionPoint!;
1364 final Matrix4 childTransform = selectables[currentSelectionEndIndex].getTransformTo(box);
1365 final Offset localDragEnd = MatrixUtils.transformPoint(
1366 childTransform,
1367 end.localPosition + Offset(0, -end.lineHeight / 2),
1368 );
1369 _currentDragEndRelatedToOrigin = MatrixUtils.transformPoint(
1370 transform,
1371 localDragEnd + deltaToOrigin,
1372 );
1373 }
1374 }
1375
1376 @override
1377 SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
1378 assert(!_selectionStartsInScrollable);
1379 final SelectionResult result = super.handleSelectAll(event);
1380 assert((currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1));
1381 if (currentSelectionStartIndex != -1) {
1382 _updateDragLocationsFromGeometries();
1383 }
1384 return result;
1385 }
1386
1387 @override
1388 SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
1389 _selectionStartsInScrollable = _globalPositionInScrollable(event.globalPosition);
1390 final SelectionResult result = super.handleSelectWord(event);
1391 _updateDragLocationsFromGeometries();
1392 return result;
1393 }
1394
1395 @override
1396 SelectionResult handleGranularlyExtendSelection(GranularlyExtendSelectionEvent event) {
1397 final SelectionResult result = super.handleGranularlyExtendSelection(event);
1398 // The selection geometry may not have the accurate offset for the edges
1399 // that are outside of the viewport whose transform may not be valid. Only
1400 // the edge this event is updating is sure to be accurate.
1401 _updateDragLocationsFromGeometries(forceUpdateStart: !event.isEnd, forceUpdateEnd: event.isEnd);
1402 if (_selectionStartsInScrollable) {
1403 _jumpToEdge(event.isEnd);
1404 }
1405 return result;
1406 }
1407
1408 @override
1409 SelectionResult handleDirectionallyExtendSelection(DirectionallyExtendSelectionEvent event) {
1410 final SelectionResult result = super.handleDirectionallyExtendSelection(event);
1411 // The selection geometry may not have the accurate offset for the edges
1412 // that are outside of the viewport whose transform may not be valid. Only
1413 // the edge this event is updating is sure to be accurate.
1414 _updateDragLocationsFromGeometries(forceUpdateStart: !event.isEnd, forceUpdateEnd: event.isEnd);
1415 if (_selectionStartsInScrollable) {
1416 _jumpToEdge(event.isEnd);
1417 }
1418 return result;
1419 }
1420
1421 void _jumpToEdge(bool isExtent) {
1422 final Selectable selectable;
1423 final double? lineHeight;
1424 final SelectionPoint? edge;
1425 if (isExtent) {
1426 selectable = selectables[currentSelectionEndIndex];
1427 edge = selectable.value.endSelectionPoint;
1428 lineHeight = selectable.value.endSelectionPoint!.lineHeight;
1429 } else {
1430 selectable = selectables[currentSelectionStartIndex];
1431 edge = selectable.value.startSelectionPoint;
1432 lineHeight = selectable.value.startSelectionPoint?.lineHeight;
1433 }
1434 if (lineHeight == null || edge == null) {
1435 return;
1436 }
1437 final RenderBox scrollableBox = state.context.findRenderObject()! as RenderBox;
1438 final Matrix4 transform = selectable.getTransformTo(scrollableBox);
1439 final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint(
1440 transform,
1441 edge.localPosition,
1442 );
1443 final Rect scrollableRect = Rect.fromLTRB(
1444 0,
1445 0,
1446 scrollableBox.size.width,
1447 scrollableBox.size.height,
1448 );
1449 switch (state.axisDirection) {
1450 case AxisDirection.up:
1451 final double edgeBottom = edgeOffsetInScrollableCoordinates.dy;
1452 final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight;
1453 if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) {
1454 return;
1455 }
1456 if (edgeBottom > scrollableRect.bottom) {
1457 position.jumpTo(position.pixels + scrollableRect.bottom - edgeBottom);
1458 return;
1459 }
1460 if (edgeTop < scrollableRect.top) {
1461 position.jumpTo(position.pixels + scrollableRect.top - edgeTop);
1462 }
1463 return;
1464 case AxisDirection.right:
1465 final double edge = edgeOffsetInScrollableCoordinates.dx;
1466 if (edge >= scrollableRect.right && edge <= scrollableRect.left) {
1467 return;
1468 }
1469 if (edge > scrollableRect.right) {
1470 position.jumpTo(position.pixels + edge - scrollableRect.right);
1471 return;
1472 }
1473 if (edge < scrollableRect.left) {
1474 position.jumpTo(position.pixels + edge - scrollableRect.left);
1475 }
1476 return;
1477 case AxisDirection.down:
1478 final double edgeBottom = edgeOffsetInScrollableCoordinates.dy;
1479 final double edgeTop = edgeOffsetInScrollableCoordinates.dy - lineHeight;
1480 if (edgeBottom >= scrollableRect.bottom && edgeTop <= scrollableRect.top) {
1481 return;
1482 }
1483 if (edgeBottom > scrollableRect.bottom) {
1484 position.jumpTo(position.pixels + edgeBottom - scrollableRect.bottom);
1485 return;
1486 }
1487 if (edgeTop < scrollableRect.top) {
1488 position.jumpTo(position.pixels + edgeTop - scrollableRect.top);
1489 }
1490 return;
1491 case AxisDirection.left:
1492 final double edge = edgeOffsetInScrollableCoordinates.dx;
1493 if (edge >= scrollableRect.right && edge <= scrollableRect.left) {
1494 return;
1495 }
1496 if (edge > scrollableRect.right) {
1497 position.jumpTo(position.pixels + scrollableRect.right - edge);
1498 return;
1499 }
1500 if (edge < scrollableRect.left) {
1501 position.jumpTo(position.pixels + scrollableRect.left - edge);
1502 }
1503 return;
1504 }
1505 }
1506
1507 bool _globalPositionInScrollable(Offset globalPosition) {
1508 final RenderBox box = state.context.findRenderObject()! as RenderBox;
1509 final Offset localPosition = box.globalToLocal(globalPosition);
1510 final Rect rect = Rect.fromLTWH(0, 0, box.size.width, box.size.height);
1511 return rect.contains(localPosition);
1512 }
1513
1514 Rect _dragTargetFromEvent(SelectionEdgeUpdateEvent event) {
1515 return Rect.fromCenter(
1516 center: event.globalPosition,
1517 width: _kDefaultDragTargetSize,
1518 height: _kDefaultDragTargetSize,
1519 );
1520 }
1521
1522 @override
1523 SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
1524 switch (event.type) {
1525 case SelectionEventType.startEdgeUpdate:
1526 _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
1527 ensureChildUpdated(selectable);
1528 case SelectionEventType.endEdgeUpdate:
1529 _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
1530 ensureChildUpdated(selectable);
1531 case SelectionEventType.granularlyExtendSelection:
1532 case SelectionEventType.directionallyExtendSelection:
1533 ensureChildUpdated(selectable);
1534 _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
1535 _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
1536 case SelectionEventType.clear:
1537 _selectableEndEdgeUpdateRecords.remove(selectable);
1538 _selectableStartEdgeUpdateRecords.remove(selectable);
1539 case SelectionEventType.selectAll:
1540 case SelectionEventType.selectWord:
1541 case SelectionEventType.selectParagraph:
1542 _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
1543 _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
1544 }
1545 return super.dispatchSelectionEventToChild(selectable, event);
1546 }
1547
1548 @override
1549 void ensureChildUpdated(Selectable selectable) {
1550 final double newRecord = state.position.pixels;
1551 final double? previousStartRecord = _selectableStartEdgeUpdateRecords[selectable];
1552 if (_currentDragStartRelatedToOrigin != null &&
1553 (previousStartRecord == null ||
1554 (newRecord - previousStartRecord).abs() > precisionErrorTolerance)) {
1555 // Make sure the selectable has up to date events.
1556 final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
1557 final Offset startOffset = _currentDragStartRelatedToOrigin!.translate(
1558 -deltaToOrigin.dx,
1559 -deltaToOrigin.dy,
1560 );
1561 selectable.dispatchSelectionEvent(
1562 SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset),
1563 );
1564 // Make sure we track that we have synthesized a start event for this selectable,
1565 // so we don't synthesize events unnecessarily.
1566 _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels;
1567 }
1568 final double? previousEndRecord = _selectableEndEdgeUpdateRecords[selectable];
1569 if (_currentDragEndRelatedToOrigin != null &&
1570 (previousEndRecord == null ||
1571 (newRecord - previousEndRecord).abs() > precisionErrorTolerance)) {
1572 // Make sure the selectable has up to date events.
1573 final Offset deltaToOrigin = _getDeltaToScrollOrigin(state);
1574 final Offset endOffset = _currentDragEndRelatedToOrigin!.translate(
1575 -deltaToOrigin.dx,
1576 -deltaToOrigin.dy,
1577 );
1578 selectable.dispatchSelectionEvent(SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset));
1579 // Make sure we track that we have synthesized an end event for this selectable,
1580 // so we don't synthesize events unnecessarily.
1581 _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels;
1582 }
1583 }
1584
1585 @override
1586 void dispose() {
1587 _selectableStartEdgeUpdateRecords.clear();
1588 _selectableEndEdgeUpdateRecords.clear();
1589 _scheduledLayoutChange = false;
1590 _autoScroller.stopAutoScroll();
1591 super.dispose();
1592 }
1593}
1594
1595Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) {
1596 return switch (scrollableState.axisDirection) {
1597 AxisDirection.up => Offset(0, -scrollableState.position.pixels),
1598 AxisDirection.down => Offset(0, scrollableState.position.pixels),
1599 AxisDirection.left => Offset(-scrollableState.position.pixels, 0),
1600 AxisDirection.right => Offset(scrollableState.position.pixels, 0),
1601 };
1602}
1603
1604/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
1605/// excluded from the scrollable area for semantics purposes.
1606///
1607/// Nodes, that are to be excluded, have to be tagged with
1608/// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in
1609/// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its
1610/// [SemanticsConfiguration] by overriding
1611/// [RenderObject.describeSemanticsConfiguration].
1612///
1613/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport,
1614/// two semantics nodes will be used to represent the [Scrollable]: The outer
1615/// node will contain all children, that are excluded from scrolling. The inner
1616/// node, which is annotated with the scrolling actions, will house the
1617/// scrollable children.
1618class _ScrollSemantics extends SingleChildRenderObjectWidget {
1619 const _ScrollSemantics({
1620 super.key,
1621 required this.position,
1622 required this.allowImplicitScrolling,
1623 required this.axis,
1624 required this.semanticChildCount,
1625 super.child,
1626 }) : assert(semanticChildCount == null || semanticChildCount >= 0);
1627
1628 final ScrollPosition position;
1629 final bool allowImplicitScrolling;
1630 final int? semanticChildCount;
1631 final Axis axis;
1632
1633 @override
1634 _RenderScrollSemantics createRenderObject(BuildContext context) {
1635 return _RenderScrollSemantics(
1636 position: position,
1637 allowImplicitScrolling: allowImplicitScrolling,
1638 semanticChildCount: semanticChildCount,
1639 axis: axis,
1640 );
1641 }
1642
1643 @override
1644 void updateRenderObject(BuildContext context, _RenderScrollSemantics renderObject) {
1645 renderObject
1646 ..allowImplicitScrolling = allowImplicitScrolling
1647 ..axis = axis
1648 ..position = position
1649 ..semanticChildCount = semanticChildCount;
1650 }
1651}
1652
1653class _RenderScrollSemantics extends RenderProxyBox {
1654 _RenderScrollSemantics({
1655 required ScrollPosition position,
1656 required bool allowImplicitScrolling,
1657 required this.axis,
1658 required int? semanticChildCount,
1659 RenderBox? child,
1660 }) : _position = position,
1661 _allowImplicitScrolling = allowImplicitScrolling,
1662 _semanticChildCount = semanticChildCount,
1663 super(child) {
1664 position.addListener(markNeedsSemanticsUpdate);
1665 }
1666
1667 /// Whether this render object is excluded from the semantic tree.
1668 ScrollPosition get position => _position;
1669 ScrollPosition _position;
1670 set position(ScrollPosition value) {
1671 if (value == _position) {
1672 return;
1673 }
1674 _position.removeListener(markNeedsSemanticsUpdate);
1675 _position = value;
1676 _position.addListener(markNeedsSemanticsUpdate);
1677 markNeedsSemanticsUpdate();
1678 }
1679
1680 /// Whether this node can be scrolled implicitly.
1681 bool get allowImplicitScrolling => _allowImplicitScrolling;
1682 bool _allowImplicitScrolling;
1683 set allowImplicitScrolling(bool value) {
1684 if (value == _allowImplicitScrolling) {
1685 return;
1686 }
1687 _allowImplicitScrolling = value;
1688 markNeedsSemanticsUpdate();
1689 }
1690
1691 Axis axis;
1692
1693 int? get semanticChildCount => _semanticChildCount;
1694 int? _semanticChildCount;
1695 set semanticChildCount(int? value) {
1696 if (value == semanticChildCount) {
1697 return;
1698 }
1699 _semanticChildCount = value;
1700 markNeedsSemanticsUpdate();
1701 }
1702
1703 void _onScrollToOffset(Offset targetOffset) {
1704 final double offset = switch (axis) {
1705 Axis.horizontal => targetOffset.dx,
1706 Axis.vertical => targetOffset.dy,
1707 };
1708 _position.jumpTo(offset);
1709 }
1710
1711 @override
1712 void describeSemanticsConfiguration(SemanticsConfiguration config) {
1713 super.describeSemanticsConfiguration(config);
1714 config.isSemanticBoundary = true;
1715 if (position.haveDimensions) {
1716 config
1717 ..hasImplicitScrolling = allowImplicitScrolling
1718 ..scrollPosition = _position.pixels
1719 ..scrollExtentMax = _position.maxScrollExtent
1720 ..scrollExtentMin = _position.minScrollExtent
1721 ..scrollChildCount = semanticChildCount;
1722 if (position.maxScrollExtent > position.minScrollExtent && allowImplicitScrolling) {
1723 config.onScrollToOffset = _onScrollToOffset;
1724 }
1725 }
1726 }
1727
1728 SemanticsNode? _innerNode;
1729
1730 @override
1731 void assembleSemanticsNode(
1732 SemanticsNode node,
1733 SemanticsConfiguration config,
1734 Iterable<SemanticsNode> children,
1735 ) {
1736 if (children.isEmpty || !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) {
1737 _innerNode = null;
1738 super.assembleSemanticsNode(node, config, children);
1739 return;
1740 }
1741
1742 (_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect;
1743
1744 int? firstVisibleIndex;
1745 final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode!];
1746 final List<SemanticsNode> included = <SemanticsNode>[];
1747 for (final SemanticsNode child in children) {
1748 assert(child.isTagged(RenderViewport.useTwoPaneSemantics));
1749 if (child.isTagged(RenderViewport.excludeFromScrolling)) {
1750 excluded.add(child);
1751 } else {
1752 if (!child.hasFlag(SemanticsFlag.isHidden)) {
1753 firstVisibleIndex ??= child.indexInParent;
1754 }
1755 included.add(child);
1756 }
1757 }
1758 config.scrollIndex = firstVisibleIndex;
1759 node.updateWith(config: null, childrenInInversePaintOrder: excluded);
1760 _innerNode!.updateWith(config: config, childrenInInversePaintOrder: included);
1761 }
1762
1763 @override
1764 void clearSemantics() {
1765 super.clearSemantics();
1766 _innerNode = null;
1767 }
1768}
1769
1770// Not using a RestorableDouble because we want to allow null values and override
1771// [enabled].
1772class _RestorableScrollOffset extends RestorableValue<double?> {
1773 @override
1774 double? createDefaultValue() => null;
1775
1776 @override
1777 void didUpdateValue(double? oldValue) {
1778 notifyListeners();
1779 }
1780
1781 @override
1782 double fromPrimitives(Object? data) {
1783 return data! as double;
1784 }
1785
1786 @override
1787 Object? toPrimitives() {
1788 return value;
1789 }
1790
1791 @override
1792 bool get enabled => value != null;
1793}
1794
1795// 2D SCROLLING
1796
1797/// Specifies how to configure the [DragGestureRecognizer]s of a
1798/// [TwoDimensionalScrollable].
1799// TODO(Piinks): Add sample code, https://github.com/flutter/flutter/issues/126298
1800enum DiagonalDragBehavior {
1801 /// This behavior will not allow for any diagonal scrolling.
1802 ///
1803 /// Drag gestures in one direction or the other will lock the input axis until
1804 /// the gesture is released.
1805 none,
1806
1807 /// This behavior will only allow diagonal scrolling on a weighted
1808 /// scale per gesture event.
1809 ///
1810 /// This means that after initially evaluating the drag gesture, the weighted
1811 /// evaluation (based on [kTouchSlop]) stands until the gesture is released.
1812 weightedEvent,
1813
1814 /// This behavior will only allow diagonal scrolling on a weighted
1815 /// scale that is evaluated throughout a gesture event.
1816 ///
1817 /// This means that during each update to the drag gesture, the scrolling
1818 /// axis will be allowed to scroll diagonally if it exceeds the
1819 /// [kTouchSlop].
1820 weightedContinuous,
1821
1822 /// This behavior allows free movement in any and all directions when
1823 /// dragging.
1824 free,
1825}
1826
1827/// A widget that manages scrolling in both the vertical and horizontal
1828/// dimensions and informs the [TwoDimensionalViewport] through which the
1829/// content is viewed.
1830///
1831/// [TwoDimensionalScrollable] implements the interaction model for a scrollable
1832/// widget in both the vertical and horizontal axes, including gesture
1833/// recognition, but does not have an opinion about how the
1834/// [TwoDimensionalViewport], which actually displays the children, is
1835/// constructed.
1836///
1837/// It's rare to construct a [TwoDimensionalScrollable] directly. Instead,
1838/// consider subclassing [TwoDimensionalScrollView], which combines scrolling,
1839/// viewporting, and a layout model in both dimensions.
1840///
1841/// See also:
1842///
1843/// * [TwoDimensionalScrollView], an abstract base class for displaying a
1844/// scrolling array of children in both directions.
1845/// * [TwoDimensionalViewport], which can be used to customize the child layout
1846/// model.
1847class TwoDimensionalScrollable extends StatefulWidget {
1848 /// Creates a widget that scrolls in two dimensions.
1849 ///
1850 /// The [horizontalDetails], [verticalDetails], and [viewportBuilder] must not
1851 /// be null.
1852 const TwoDimensionalScrollable({
1853 super.key,
1854 required this.horizontalDetails,
1855 required this.verticalDetails,
1856 required this.viewportBuilder,
1857 this.incrementCalculator,
1858 this.restorationId,
1859 this.excludeFromSemantics = false,
1860 this.diagonalDragBehavior = DiagonalDragBehavior.none,
1861 this.dragStartBehavior = DragStartBehavior.start,
1862 this.hitTestBehavior = HitTestBehavior.opaque,
1863 });
1864
1865 /// How scrolling gestures should lock to one axis, or allow free movement
1866 /// in both axes.
1867 final DiagonalDragBehavior diagonalDragBehavior;
1868
1869 /// The configuration of the horizontal [Scrollable].
1870 ///
1871 /// These [ScrollableDetails] can be used to set the [AxisDirection],
1872 /// [ScrollController], [ScrollPhysics] and more for the horizontal axis.
1873 final ScrollableDetails horizontalDetails;
1874
1875 /// The configuration of the vertical [Scrollable].
1876 ///
1877 /// These [ScrollableDetails] can be used to set the [AxisDirection],
1878 /// [ScrollController], [ScrollPhysics] and more for the vertical axis.
1879 final ScrollableDetails verticalDetails;
1880
1881 /// Builds the viewport through which the scrollable content is displayed.
1882 ///
1883 /// A [TwoDimensionalViewport] uses two given [ViewportOffset]s to determine
1884 /// which part of its content is actually visible through the viewport.
1885 ///
1886 /// See also:
1887 ///
1888 /// * [TwoDimensionalViewport], which is a viewport that displays a span of
1889 /// widgets in both dimensions.
1890 final TwoDimensionalViewportBuilder viewportBuilder;
1891
1892 /// {@macro flutter.widgets.Scrollable.incrementCalculator}
1893 ///
1894 /// This value applies in both axes.
1895 final ScrollIncrementCalculator? incrementCalculator;
1896
1897 /// {@macro flutter.widgets.scrollable.restorationId}
1898 ///
1899 /// Internally, the [TwoDimensionalScrollable] will introduce a
1900 /// [RestorationScope] that will be assigned this value. The two [Scrollable]s
1901 /// within will then be given unique IDs within this scope.
1902 final String? restorationId;
1903
1904 /// {@macro flutter.widgets.scrollable.excludeFromSemantics}
1905 ///
1906 /// This value applies to both axes.
1907 final bool excludeFromSemantics;
1908
1909 /// {@macro flutter.widgets.scrollable.hitTestBehavior}
1910 ///
1911 /// This value applies to both axes.
1912 final HitTestBehavior hitTestBehavior;
1913
1914 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
1915 ///
1916 /// This value applies in both axes.
1917 final DragStartBehavior dragStartBehavior;
1918
1919 @override
1920 State<TwoDimensionalScrollable> createState() => TwoDimensionalScrollableState();
1921
1922 /// The state from the closest instance of this class that encloses the given
1923 /// context, or null if none is found.
1924 ///
1925 /// Typical usage is as follows:
1926 ///
1927 /// ```dart
1928 /// TwoDimensionalScrollableState? scrollable = TwoDimensionalScrollable.maybeOf(context);
1929 /// ```
1930 ///
1931 /// Calling this method will create a dependency on the closest
1932 /// [TwoDimensionalScrollable] in the [context]. The internal [Scrollable]s
1933 /// can be accessed through [TwoDimensionalScrollableState.verticalScrollable]
1934 /// and [TwoDimensionalScrollableState.horizontalScrollable].
1935 ///
1936 /// Alternatively, [Scrollable.maybeOf] can be used by providing the desired
1937 /// [Axis] to the `axis` parameter.
1938 ///
1939 /// See also:
1940 ///
1941 /// * [TwoDimensionalScrollable.of], which is similar to this method, but
1942 /// asserts if no [Scrollable] ancestor is found.
1943 static TwoDimensionalScrollableState? maybeOf(BuildContext context) {
1944 final _TwoDimensionalScrollableScope? widget =
1945 context.dependOnInheritedWidgetOfExactType<_TwoDimensionalScrollableScope>();
1946 return widget?.twoDimensionalScrollable;
1947 }
1948
1949 /// The state from the closest instance of this class that encloses the given
1950 /// context.
1951 ///
1952 /// Typical usage is as follows:
1953 ///
1954 /// ```dart
1955 /// TwoDimensionalScrollableState scrollable = TwoDimensionalScrollable.of(context);
1956 /// ```
1957 ///
1958 /// Calling this method will create a dependency on the closest
1959 /// [TwoDimensionalScrollable] in the [context]. The internal [Scrollable]s
1960 /// can be accessed through [TwoDimensionalScrollableState.verticalScrollable]
1961 /// and [TwoDimensionalScrollableState.horizontalScrollable].
1962 ///
1963 /// If no [TwoDimensionalScrollable] ancestor is found, then this method will
1964 /// assert in debug mode, and throw an exception in release mode.
1965 ///
1966 /// Alternatively, [Scrollable.of] can be used by providing the desired [Axis]
1967 /// to the `axis` parameter.
1968 ///
1969 /// See also:
1970 ///
1971 /// * [TwoDimensionalScrollable.maybeOf], which is similar to this method,
1972 /// but returns null if no [TwoDimensionalScrollable] ancestor is found.
1973 static TwoDimensionalScrollableState of(BuildContext context) {
1974 final TwoDimensionalScrollableState? scrollableState = maybeOf(context);
1975 assert(() {
1976 if (scrollableState == null) {
1977 throw FlutterError.fromParts(<DiagnosticsNode>[
1978 ErrorSummary(
1979 'TwoDimensionalScrollable.of() was called with a context that does '
1980 'not contain a TwoDimensionalScrollable widget.\n',
1981 ),
1982 ErrorDescription(
1983 'No TwoDimensionalScrollable widget ancestor could be found starting '
1984 'from the context that was passed to TwoDimensionalScrollable.of(). '
1985 'This can happen because you are using a widget that looks for a '
1986 'TwoDimensionalScrollable ancestor, but no such ancestor exists.\n'
1987 'The context used was:\n'
1988 ' $context',
1989 ),
1990 ]);
1991 }
1992 return true;
1993 }());
1994 return scrollableState!;
1995 }
1996}
1997
1998/// State object for a [TwoDimensionalScrollable] widget.
1999///
2000/// To manipulate one of the internal [Scrollable] widget's scroll position, use
2001/// the object obtained from the [verticalScrollable] or [horizontalScrollable]
2002/// property.
2003///
2004/// To be informed of when a [TwoDimensionalScrollable] widget is scrolling,
2005/// use a [NotificationListener] to listen for [ScrollNotification]s.
2006/// Both axes will have the same viewport depth since there is only one
2007/// viewport, and so should be differentiated by the [Axis] of the
2008/// [ScrollMetrics] provided by the notification.
2009class TwoDimensionalScrollableState extends State<TwoDimensionalScrollable> {
2010 ScrollController? _verticalFallbackController;
2011 ScrollController? _horizontalFallbackController;
2012 final GlobalKey<ScrollableState> _verticalOuterScrollableKey = GlobalKey<ScrollableState>();
2013 final GlobalKey<ScrollableState> _horizontalInnerScrollableKey = GlobalKey<ScrollableState>();
2014
2015 /// The [ScrollableState] of the vertical axis.
2016 ///
2017 /// Accessible by calling [TwoDimensionalScrollable.of].
2018 ///
2019 /// Alternatively, [Scrollable.of] can be used by providing [Axis.vertical]
2020 /// to the `axis` parameter.
2021 ScrollableState get verticalScrollable {
2022 assert(_verticalOuterScrollableKey.currentState != null);
2023 return _verticalOuterScrollableKey.currentState!;
2024 }
2025
2026 /// The [ScrollableState] of the horizontal axis.
2027 ///
2028 /// Accessible by calling [TwoDimensionalScrollable.of].
2029 ///
2030 /// Alternatively, [Scrollable.of] can be used by providing [Axis.horizontal]
2031 /// to the `axis` parameter.
2032 ScrollableState get horizontalScrollable {
2033 assert(_horizontalInnerScrollableKey.currentState != null);
2034 return _horizontalInnerScrollableKey.currentState!;
2035 }
2036
2037 @protected
2038 @override
2039 void initState() {
2040 if (widget.verticalDetails.controller == null) {
2041 _verticalFallbackController = ScrollController();
2042 }
2043 if (widget.horizontalDetails.controller == null) {
2044 _horizontalFallbackController = ScrollController();
2045 }
2046 super.initState();
2047 }
2048
2049 @protected
2050 @override
2051 void didUpdateWidget(TwoDimensionalScrollable oldWidget) {
2052 super.didUpdateWidget(oldWidget);
2053 // Handle changes in the provided/fallback scroll controllers
2054
2055 // Vertical
2056 if (oldWidget.verticalDetails.controller != widget.verticalDetails.controller) {
2057 if (oldWidget.verticalDetails.controller == null) {
2058 // The old controller was null, meaning the fallback cannot be null.
2059 // Dispose of the fallback.
2060 assert(_verticalFallbackController != null);
2061 assert(widget.verticalDetails.controller != null);
2062 _verticalFallbackController!.dispose();
2063 _verticalFallbackController = null;
2064 } else if (widget.verticalDetails.controller == null) {
2065 // If the new controller is null, we need to set up the fallback
2066 // ScrollController.
2067 assert(_verticalFallbackController == null);
2068 _verticalFallbackController = ScrollController();
2069 }
2070 }
2071
2072 // Horizontal
2073 if (oldWidget.horizontalDetails.controller != widget.horizontalDetails.controller) {
2074 if (oldWidget.horizontalDetails.controller == null) {
2075 // The old controller was null, meaning the fallback cannot be null.
2076 // Dispose of the fallback.
2077 assert(_horizontalFallbackController != null);
2078 assert(widget.horizontalDetails.controller != null);
2079 _horizontalFallbackController!.dispose();
2080 _horizontalFallbackController = null;
2081 } else if (widget.horizontalDetails.controller == null) {
2082 // If the new controller is null, we need to set up the fallback
2083 // ScrollController.
2084 assert(_horizontalFallbackController == null);
2085 _horizontalFallbackController = ScrollController();
2086 }
2087 }
2088 }
2089
2090 @protected
2091 @override
2092 Widget build(BuildContext context) {
2093 assert(
2094 axisDirectionToAxis(widget.verticalDetails.direction) == Axis.vertical,
2095 'TwoDimensionalScrollable.verticalDetails are not Axis.vertical.',
2096 );
2097 assert(
2098 axisDirectionToAxis(widget.horizontalDetails.direction) == Axis.horizontal,
2099 'TwoDimensionalScrollable.horizontalDetails are not Axis.horizontal.',
2100 );
2101
2102 final Widget result = RestorationScope(
2103 restorationId: widget.restorationId,
2104 child: _VerticalOuterDimension(
2105 key: _verticalOuterScrollableKey,
2106 // For gesture forwarding
2107 horizontalKey: _horizontalInnerScrollableKey,
2108 axisDirection: widget.verticalDetails.direction,
2109 controller: widget.verticalDetails.controller ?? _verticalFallbackController!,
2110 physics: widget.verticalDetails.physics,
2111 clipBehavior:
2112 widget.verticalDetails.clipBehavior ??
2113 widget.verticalDetails.decorationClipBehavior ??
2114 Clip.hardEdge,
2115 incrementCalculator: widget.incrementCalculator,
2116 excludeFromSemantics: widget.excludeFromSemantics,
2117 restorationId: 'OuterVerticalTwoDimensionalScrollable',
2118 dragStartBehavior: widget.dragStartBehavior,
2119 diagonalDragBehavior: widget.diagonalDragBehavior,
2120 hitTestBehavior: widget.hitTestBehavior,
2121 viewportBuilder: (BuildContext context, ViewportOffset verticalOffset) {
2122 return _HorizontalInnerDimension(
2123 key: _horizontalInnerScrollableKey,
2124 verticalOuterKey: _verticalOuterScrollableKey,
2125 axisDirection: widget.horizontalDetails.direction,
2126 controller: widget.horizontalDetails.controller ?? _horizontalFallbackController!,
2127 physics: widget.horizontalDetails.physics,
2128 clipBehavior:
2129 widget.horizontalDetails.clipBehavior ??
2130 widget.horizontalDetails.decorationClipBehavior ??
2131 Clip.hardEdge,
2132 incrementCalculator: widget.incrementCalculator,
2133 excludeFromSemantics: widget.excludeFromSemantics,
2134 restorationId: 'InnerHorizontalTwoDimensionalScrollable',
2135 dragStartBehavior: widget.dragStartBehavior,
2136 diagonalDragBehavior: widget.diagonalDragBehavior,
2137 hitTestBehavior: widget.hitTestBehavior,
2138 viewportBuilder: (BuildContext context, ViewportOffset horizontalOffset) {
2139 return widget.viewportBuilder(context, verticalOffset, horizontalOffset);
2140 },
2141 );
2142 },
2143 ),
2144 );
2145
2146 // TODO(Piinks): Build scrollbars for 2 dimensions instead of 1,
2147 // https://github.com/flutter/flutter/issues/122348
2148
2149 return _TwoDimensionalScrollableScope(twoDimensionalScrollable: this, child: result);
2150 }
2151
2152 @protected
2153 @override
2154 void dispose() {
2155 _verticalFallbackController?.dispose();
2156 _horizontalFallbackController?.dispose();
2157 super.dispose();
2158 }
2159}
2160
2161// Enable TwoDimensionalScrollable.of() to work as if
2162// TwoDimensionalScrollableState was an inherited widget.
2163// TwoDimensionalScrollableState.build() always rebuilds its
2164// _TwoDimensionalScrollableScope.
2165class _TwoDimensionalScrollableScope extends InheritedWidget {
2166 const _TwoDimensionalScrollableScope({
2167 required this.twoDimensionalScrollable,
2168 required super.child,
2169 });
2170
2171 final TwoDimensionalScrollableState twoDimensionalScrollable;
2172
2173 @override
2174 bool updateShouldNotify(_TwoDimensionalScrollableScope old) => false;
2175}
2176
2177// Vertical outer scrollable of 2D scrolling
2178class _VerticalOuterDimension extends Scrollable {
2179 const _VerticalOuterDimension({
2180 super.key,
2181 required this.horizontalKey,
2182 required super.viewportBuilder,
2183 required super.axisDirection,
2184 super.controller,
2185 super.physics,
2186 super.clipBehavior,
2187 super.incrementCalculator,
2188 super.excludeFromSemantics,
2189 super.dragStartBehavior,
2190 super.restorationId,
2191 super.hitTestBehavior,
2192 this.diagonalDragBehavior = DiagonalDragBehavior.none,
2193 }) : assert(axisDirection == AxisDirection.up || axisDirection == AxisDirection.down);
2194
2195 final DiagonalDragBehavior diagonalDragBehavior;
2196 final GlobalKey<ScrollableState> horizontalKey;
2197
2198 @override
2199 _VerticalOuterDimensionState createState() => _VerticalOuterDimensionState();
2200}
2201
2202class _VerticalOuterDimensionState extends ScrollableState {
2203 DiagonalDragBehavior get diagonalDragBehavior =>
2204 (widget as _VerticalOuterDimension).diagonalDragBehavior;
2205 ScrollableState get horizontalScrollable =>
2206 (widget as _VerticalOuterDimension).horizontalKey.currentState!;
2207
2208 Axis? lockedAxis;
2209 Offset? lastDragOffset;
2210
2211 // Implemented in the _HorizontalInnerDimension instead.
2212 @override
2213 _EnsureVisibleResults _performEnsureVisible(
2214 RenderObject object, {
2215 double alignment = 0.0,
2216 Duration duration = Duration.zero,
2217 Curve curve = Curves.ease,
2218 ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
2219 RenderObject? targetRenderObject,
2220 }) {
2221 assert(
2222 false,
2223 'The _performEnsureVisible method was called for the vertical scrollable '
2224 'of a TwoDimensionalScrollable. This should not happen as the horizontal '
2225 'scrollable handles both axes.',
2226 );
2227 return (<Future<void>>[], this);
2228 }
2229
2230 void _evaluateLockedAxis(Offset offset) {
2231 assert(lastDragOffset != null);
2232 final Offset offsetDelta = lastDragOffset! - offset;
2233 final double axisDifferential = offsetDelta.dx.abs() - offsetDelta.dy.abs();
2234 if (axisDifferential.abs() >= kTouchSlop) {
2235 // We have single axis winner.
2236 lockedAxis = axisDifferential > 0.0 ? Axis.horizontal : Axis.vertical;
2237 } else {
2238 lockedAxis = null;
2239 }
2240 }
2241
2242 @override
2243 void _handleDragDown(DragDownDetails details) {
2244 switch (diagonalDragBehavior) {
2245 case DiagonalDragBehavior.none:
2246 break;
2247 case DiagonalDragBehavior.weightedEvent:
2248 case DiagonalDragBehavior.weightedContinuous:
2249 case DiagonalDragBehavior.free:
2250 // Initiate hold. If one or the other wins the gesture, cancel the
2251 // opposite axis.
2252 horizontalScrollable._handleDragDown(details);
2253 }
2254 super._handleDragDown(details);
2255 }
2256
2257 @override
2258 void _handleDragStart(DragStartDetails details) {
2259 lastDragOffset = details.globalPosition;
2260 switch (diagonalDragBehavior) {
2261 case DiagonalDragBehavior.none:
2262 break;
2263 case DiagonalDragBehavior.free:
2264 // Prepare to scroll both.
2265 // vertical - will call super below after switch.
2266 horizontalScrollable._handleDragStart(details);
2267 case DiagonalDragBehavior.weightedEvent:
2268 case DiagonalDragBehavior.weightedContinuous:
2269 // See if one axis wins the drag.
2270 _evaluateLockedAxis(details.globalPosition);
2271 switch (lockedAxis) {
2272 case null:
2273 // Prepare to scroll both, null means no winner yet.
2274 // vertical - will call super below after switch.
2275 horizontalScrollable._handleDragStart(details);
2276 case Axis.horizontal:
2277 // Prepare to scroll horizontally.
2278 horizontalScrollable._handleDragStart(details);
2279 return;
2280 case Axis.vertical:
2281 // Prepare to scroll vertically - will call super below after switch.
2282 }
2283 }
2284 super._handleDragStart(details);
2285 }
2286
2287 @override
2288 void _handleDragUpdate(DragUpdateDetails details) {
2289 final DragUpdateDetails verticalDragDetails = DragUpdateDetails(
2290 sourceTimeStamp: details.sourceTimeStamp,
2291 delta: Offset(0.0, details.delta.dy),
2292 primaryDelta: details.delta.dy,
2293 globalPosition: details.globalPosition,
2294 localPosition: details.localPosition,
2295 );
2296 final DragUpdateDetails horizontalDragDetails = DragUpdateDetails(
2297 sourceTimeStamp: details.sourceTimeStamp,
2298 delta: Offset(details.delta.dx, 0.0),
2299 primaryDelta: details.delta.dx,
2300 globalPosition: details.globalPosition,
2301 localPosition: details.localPosition,
2302 );
2303
2304 switch (diagonalDragBehavior) {
2305 case DiagonalDragBehavior.none:
2306 // Default gesture handling from super class.
2307 super._handleDragUpdate(verticalDragDetails);
2308 return;
2309 case DiagonalDragBehavior.free:
2310 // Scroll both axes
2311 horizontalScrollable._handleDragUpdate(horizontalDragDetails);
2312 super._handleDragUpdate(verticalDragDetails);
2313 return;
2314 case DiagonalDragBehavior.weightedContinuous:
2315 // Re-evaluate locked axis for every update.
2316 _evaluateLockedAxis(details.globalPosition);
2317 lastDragOffset = details.globalPosition;
2318 case DiagonalDragBehavior.weightedEvent:
2319 // Lock axis only once per gesture.
2320 if (lockedAxis == null && lastDragOffset != null) {
2321 // A winner has not been declared yet.
2322 // See if one axis has won the drag.
2323 _evaluateLockedAxis(details.globalPosition);
2324 }
2325 }
2326 switch (lockedAxis) {
2327 case null:
2328 // Scroll both - vertical after switch
2329 horizontalScrollable._handleDragUpdate(horizontalDragDetails);
2330 case Axis.horizontal:
2331 // Scroll horizontally
2332 horizontalScrollable._handleDragUpdate(horizontalDragDetails);
2333 return;
2334 case Axis.vertical:
2335 // Scroll vertically - after switch
2336 }
2337 super._handleDragUpdate(verticalDragDetails);
2338 }
2339
2340 @override
2341 void _handleDragEnd(DragEndDetails details) {
2342 lastDragOffset = null;
2343 lockedAxis = null;
2344 final double dx = details.velocity.pixelsPerSecond.dx;
2345 final double dy = details.velocity.pixelsPerSecond.dy;
2346 final DragEndDetails verticalDragDetails = DragEndDetails(
2347 velocity: Velocity(pixelsPerSecond: Offset(0.0, dy)),
2348 primaryVelocity: dy,
2349 );
2350 final DragEndDetails horizontalDragDetails = DragEndDetails(
2351 velocity: Velocity(pixelsPerSecond: Offset(dx, 0.0)),
2352 primaryVelocity: dx,
2353 );
2354
2355 switch (diagonalDragBehavior) {
2356 case DiagonalDragBehavior.none:
2357 break;
2358 case DiagonalDragBehavior.weightedEvent:
2359 case DiagonalDragBehavior.weightedContinuous:
2360 case DiagonalDragBehavior.free:
2361 horizontalScrollable._handleDragEnd(horizontalDragDetails);
2362 }
2363 super._handleDragEnd(verticalDragDetails);
2364 }
2365
2366 @override
2367 void _handleDragCancel() {
2368 lastDragOffset = null;
2369 lockedAxis = null;
2370 switch (diagonalDragBehavior) {
2371 case DiagonalDragBehavior.none:
2372 break;
2373 case DiagonalDragBehavior.weightedEvent:
2374 case DiagonalDragBehavior.weightedContinuous:
2375 case DiagonalDragBehavior.free:
2376 horizontalScrollable._handleDragCancel();
2377 }
2378 super._handleDragCancel();
2379 }
2380
2381 @override
2382 void setCanDrag(bool value) {
2383 switch (diagonalDragBehavior) {
2384 case DiagonalDragBehavior.none:
2385 // If we aren't scrolling diagonally, the default drag gesture recognizer
2386 // is used.
2387 super.setCanDrag(value);
2388 return;
2389 case DiagonalDragBehavior.weightedEvent:
2390 case DiagonalDragBehavior.weightedContinuous:
2391 case DiagonalDragBehavior.free:
2392 if (value) {
2393 // Replaces the typical vertical/horizontal drag gesture recognizers
2394 // with a pan gesture recognizer to allow bidirectional scrolling.
2395 // Based on the diagonalDragBehavior, valid vertical deltas are
2396 // applied to this scrollable, while horizontal deltas are routed to
2397 // the horizontal scrollable.
2398 _gestureRecognizers = <Type, GestureRecognizerFactory>{
2399 PanGestureRecognizer: GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
2400 () => PanGestureRecognizer(supportedDevices: _configuration.dragDevices),
2401 (PanGestureRecognizer instance) {
2402 instance
2403 ..onDown = _handleDragDown
2404 ..onStart = _handleDragStart
2405 ..onUpdate = _handleDragUpdate
2406 ..onEnd = _handleDragEnd
2407 ..onCancel = _handleDragCancel
2408 ..minFlingDistance = _physics?.minFlingDistance
2409 ..minFlingVelocity = _physics?.minFlingVelocity
2410 ..maxFlingVelocity = _physics?.maxFlingVelocity
2411 ..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
2412 ..dragStartBehavior = widget.dragStartBehavior
2413 ..gestureSettings = _mediaQueryGestureSettings;
2414 },
2415 ),
2416 };
2417 // Cancel the active hold/drag (if any) because the gesture recognizers
2418 // will soon be disposed by our RawGestureDetector, and we won't be
2419 // receiving pointer up events to cancel the hold/drag.
2420 _handleDragCancel();
2421 _lastCanDrag = value;
2422 _lastAxisDirection = widget.axis;
2423 if (_gestureDetectorKey.currentState != null) {
2424 _gestureDetectorKey.currentState!.replaceGestureRecognizers(_gestureRecognizers);
2425 }
2426 }
2427 return;
2428 }
2429 }
2430
2431 @override
2432 Widget _buildChrome(BuildContext context, Widget child) {
2433 final ScrollableDetails details = ScrollableDetails(
2434 direction: widget.axisDirection,
2435 controller: _effectiveScrollController,
2436 clipBehavior: widget.clipBehavior,
2437 );
2438 // Skip building a scrollbar here, the dual scrollbar is added in
2439 // TwoDimensionalScrollableState.
2440 return _configuration.buildOverscrollIndicator(context, child, details);
2441 }
2442}
2443
2444// Horizontal inner scrollable of 2D scrolling
2445class _HorizontalInnerDimension extends Scrollable {
2446 const _HorizontalInnerDimension({
2447 super.key,
2448 required this.verticalOuterKey,
2449 required super.viewportBuilder,
2450 required super.axisDirection,
2451 super.controller,
2452 super.physics,
2453 super.clipBehavior,
2454 super.incrementCalculator,
2455 super.excludeFromSemantics,
2456 super.dragStartBehavior,
2457 super.restorationId,
2458 super.hitTestBehavior,
2459 this.diagonalDragBehavior = DiagonalDragBehavior.none,
2460 }) : assert(axisDirection == AxisDirection.left || axisDirection == AxisDirection.right);
2461
2462 final GlobalKey<ScrollableState> verticalOuterKey;
2463 final DiagonalDragBehavior diagonalDragBehavior;
2464
2465 @override
2466 _HorizontalInnerDimensionState createState() => _HorizontalInnerDimensionState();
2467}
2468
2469class _HorizontalInnerDimensionState extends ScrollableState {
2470 late ScrollableState verticalScrollable;
2471
2472 GlobalKey<ScrollableState> get verticalOuterKey =>
2473 (widget as _HorizontalInnerDimension).verticalOuterKey;
2474 DiagonalDragBehavior get diagonalDragBehavior =>
2475 (widget as _HorizontalInnerDimension).diagonalDragBehavior;
2476
2477 @override
2478 void didChangeDependencies() {
2479 verticalScrollable = Scrollable.of(context);
2480 assert(axisDirectionToAxis(verticalScrollable.axisDirection) == Axis.vertical);
2481 super.didChangeDependencies();
2482 }
2483
2484 // Returns the Future from calling ensureVisible for the ScrollPosition, as
2485 // as well as the vertical ScrollableState instance so its context can be
2486 // used to check for other ancestor Scrollables in executing ensureVisible.
2487 @override
2488 _EnsureVisibleResults _performEnsureVisible(
2489 RenderObject object, {
2490 double alignment = 0.0,
2491 Duration duration = Duration.zero,
2492 Curve curve = Curves.ease,
2493 ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
2494 RenderObject? targetRenderObject,
2495 }) {
2496 final List<Future<void>> newFutures = <Future<void>>[
2497 position.ensureVisible(
2498 object,
2499 alignment: alignment,
2500 duration: duration,
2501 curve: curve,
2502 alignmentPolicy: alignmentPolicy,
2503 ),
2504 verticalScrollable.position.ensureVisible(
2505 object,
2506 alignment: alignment,
2507 duration: duration,
2508 curve: curve,
2509 alignmentPolicy: alignmentPolicy,
2510 ),
2511 ];
2512
2513 return (newFutures, verticalScrollable);
2514 }
2515
2516 @override
2517 void setCanDrag(bool value) {
2518 switch (diagonalDragBehavior) {
2519 case DiagonalDragBehavior.none:
2520 // If we aren't scrolling diagonally, the default drag gesture
2521 // recognizer is used.
2522 super.setCanDrag(value);
2523 return;
2524 case DiagonalDragBehavior.weightedEvent:
2525 case DiagonalDragBehavior.weightedContinuous:
2526 case DiagonalDragBehavior.free:
2527 if (value) {
2528 // If a type of diagonal scrolling is enabled, a panning gesture
2529 // recognizer will be created for the _VerticalOuterDimension. So in
2530 // this case, the _HorizontalInnerDimension does not require a gesture
2531 // recognizer, meanwhile we should ensure the outer dimension has
2532 // updated in case it did not have enough content to enable dragging.
2533 _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
2534 verticalOuterKey.currentState!.setCanDrag(value);
2535 // Cancel the active hold/drag (if any) because the gesture recognizers
2536 // will soon be disposed by our RawGestureDetector, and we won't be
2537 // receiving pointer up events to cancel the hold/drag.
2538 _handleDragCancel();
2539 _lastCanDrag = value;
2540 _lastAxisDirection = widget.axis;
2541 if (_gestureDetectorKey.currentState != null) {
2542 _gestureDetectorKey.currentState!.replaceGestureRecognizers(_gestureRecognizers);
2543 }
2544 }
2545 return;
2546 }
2547 }
2548
2549 @override
2550 Widget _buildChrome(BuildContext context, Widget child) {
2551 final ScrollableDetails details = ScrollableDetails(
2552 direction: widget.axisDirection,
2553 controller: _effectiveScrollController,
2554 clipBehavior: widget.clipBehavior,
2555 );
2556 // Skip building a scrollbar here, the dual scrollbar is added in
2557 // TwoDimensionalScrollableState.
2558 return _configuration.buildOverscrollIndicator(context, child, details);
2559 }
2560}
2561

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com