1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/painting.dart'; |
7 | |
8 | import 'framework.dart'; |
9 | import 'scroll_configuration.dart'; |
10 | import 'scroll_controller.dart'; |
11 | |
12 | const 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]. |
48 | class 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 | |