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