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 'package:flutter/foundation.dart';
6import 'package:flutter/painting.dart';
7
8import 'framework.dart';
9import 'scroll_configuration.dart';
10import 'scroll_controller.dart';
11
12const Set<TargetPlatform> _kMobilePlatforms = <TargetPlatform>{
13 TargetPlatform.android,
14 TargetPlatform.iOS,
15 TargetPlatform.fuchsia,
16};
17
18/// Associates a [ScrollController] with a subtree.
19///
20/// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU}
21///
22/// When a [ScrollView] has [ScrollView.primary] set to true, the [ScrollView]
23/// uses [of] to inherit the [PrimaryScrollController] associated with its
24/// subtree.
25///
26/// A ScrollView that doesn't have a controller or the primary flag set will
27/// inherit the PrimaryScrollController, if [shouldInherit] allows it. By
28/// default [shouldInherit] is true for mobile platforms when the ScrollView has
29/// a scroll direction of [Axis.vertical]. This automatic inheritance can be
30/// configured with [automaticallyInheritForPlatforms] and [scrollDirection].
31///
32/// Inheriting this ScrollController can provide default behavior for scroll
33/// views in a subtree. For example, the [Scaffold] uses this mechanism to
34/// implement the scroll-to-top gesture on iOS.
35///
36/// Another default behavior handled by the PrimaryScrollController is default
37/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
38/// part of the application, the ScrollAction will be evaluated using the scroll
39/// view associated with a PrimaryScrollController, for example, when executing
40/// [Shortcuts] key events like page up and down.
41///
42/// See also:
43/// * [ScrollAction], an [Action] that scrolls the [Scrollable] that encloses
44/// the current [primaryFocus] or is attached to the PrimaryScrollController.
45/// * [Shortcuts], a widget that establishes a [ShortcutManager] to be used
46/// by its descendants when invoking an [Action] via a keyboard key
47/// combination that maps to an [Intent].
48class PrimaryScrollController extends InheritedWidget {
49 /// Creates a widget that associates a [ScrollController] with a subtree.
50 const PrimaryScrollController({
51 super.key,
52 required ScrollController this.controller,
53 this.automaticallyInheritForPlatforms = _kMobilePlatforms,
54 this.scrollDirection = Axis.vertical,
55 required super.child,
56 });
57
58 /// Creates a subtree without an associated [ScrollController].
59 const PrimaryScrollController.none({
60 super.key,
61 required super.child,
62 }) : automaticallyInheritForPlatforms = const <TargetPlatform>{},
63 scrollDirection = null,
64 controller = null;
65
66 /// The [ScrollController] associated with the subtree.
67 ///
68 /// See also:
69 ///
70 /// * [ScrollView.controller], which discusses the purpose of specifying a
71 /// scroll controller.
72 final ScrollController? controller;
73
74 /// The [Axis] this controller is configured for [ScrollView]s to
75 /// automatically inherit.
76 ///
77 /// Used in conjunction with [automaticallyInheritForPlatforms]. If the
78 /// current [TargetPlatform] is not included in
79 /// [automaticallyInheritForPlatforms], this is ignored.
80 ///
81 /// When null, no [ScrollView] in any Axis will automatically inherit this
82 /// controller. This is dissimilar to [PrimaryScrollController.none]. When a
83 /// PrimaryScrollController is inherited, ScrollView will insert
84 /// PrimaryScrollController.none into the tree to prevent further descendant
85 /// ScrollViews from inheriting the current PrimaryScrollController.
86 ///
87 /// For the direction in which active scrolling may be occurring, see
88 /// [ScrollDirection].
89 ///
90 /// Defaults to [Axis.vertical].
91 final Axis? scrollDirection;
92
93 /// The [TargetPlatform]s this controller is configured for [ScrollView]s to
94 /// automatically inherit.
95 ///
96 /// Used in conjunction with [scrollDirection]. If the [Axis] provided to
97 /// [shouldInherit] is not [scrollDirection], this is ignored.
98 ///
99 /// When empty, no ScrollView in any Axis will automatically inherit this
100 /// controller. Defaults to [TargetPlatformVariant.mobile].
101 final Set<TargetPlatform> automaticallyInheritForPlatforms;
102
103 /// Returns true if this PrimaryScrollController is configured to be
104 /// automatically inherited for the current [TargetPlatform] and the given
105 /// [Axis].
106 ///
107 /// This method is typically not called directly. [ScrollView] will call this
108 /// method if it has not been provided a [ScrollController] and
109 /// [ScrollView.primary] is unset.
110 ///
111 /// If a ScrollController has already been provided to
112 /// [ScrollView.controller], or [ScrollView.primary] is set, this is method is
113 /// not called by ScrollView as it will have determined whether or not to
114 /// inherit the PrimaryScrollController.
115 static bool shouldInherit(BuildContext context, Axis scrollDirection) {
116 final PrimaryScrollController? result = context.findAncestorWidgetOfExactType<PrimaryScrollController>();
117 if (result == null) {
118 return false;
119 }
120
121 final TargetPlatform platform = ScrollConfiguration.of(context).getPlatform(context);
122 if (result.automaticallyInheritForPlatforms.contains(platform)) {
123 return result.scrollDirection == scrollDirection;
124 }
125 return false;
126 }
127
128 /// Returns the [ScrollController] most closely associated with the given
129 /// context.
130 ///
131 /// Returns null if there is no [ScrollController] associated with the given
132 /// context.
133 ///
134 /// Calling this method will create a dependency on the closest
135 /// [PrimaryScrollController] in the [context], if there is one.
136 ///
137 /// See also:
138 ///
139 /// * [PrimaryScrollController.maybeOf], which is similar to this method, but
140 /// asserts if no [PrimaryScrollController] ancestor is found.
141 static ScrollController? maybeOf(BuildContext context) {
142 final PrimaryScrollController? result = context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
143 return result?.controller;
144 }
145
146 /// Returns the [ScrollController] most closely associated with the given
147 /// context.
148 ///
149 /// If no ancestor is found, this method will assert in debug mode, and throw
150 /// an exception in release mode.
151 ///
152 /// Calling this method will create a dependency on the closest
153 /// [PrimaryScrollController] in the [context].
154 ///
155 /// See also:
156 ///
157 /// * [PrimaryScrollController.maybeOf], which is similar to this method, but
158 /// returns null if no [PrimaryScrollController] ancestor is found.
159 static ScrollController of(BuildContext context) {
160 final ScrollController? controller = maybeOf(context);
161 assert(() {
162 if (controller == null) {
163 throw FlutterError(
164 'PrimaryScrollController.of() was called with a context that does not contain a '
165 'PrimaryScrollController widget.\n'
166 'No PrimaryScrollController widget ancestor could be found starting from the '
167 'context that was passed to PrimaryScrollController.of(). This can happen '
168 'because you are using a widget that looks for a PrimaryScrollController '
169 'ancestor, but no such ancestor exists.\n'
170 'The context used was:\n'
171 ' $context',
172 );
173 }
174 return true;
175 }());
176 return controller!;
177 }
178
179 @override
180 bool updateShouldNotify(PrimaryScrollController oldWidget) => controller != oldWidget.controller;
181
182 @override
183 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
184 super.debugFillProperties(properties);
185 properties.add(DiagnosticsProperty<ScrollController>('controller', controller, ifNull: 'no controller', showName: false));
186 }
187}
188