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