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/gestures.dart';
7import 'package:flutter/rendering.dart';
8import 'package:flutter/services.dart' show LogicalKeyboardKey;
9
10import 'framework.dart';
11import 'overscroll_indicator.dart';
12import 'scroll_physics.dart';
13import 'scrollable.dart';
14import 'scrollable_helpers.dart';
15import 'scrollbar.dart';
16
17const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
18
19/// Device types that scrollables should accept drag gestures from by default.
20const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
21 PointerDeviceKind.touch,
22 PointerDeviceKind.stylus,
23 PointerDeviceKind.invertedStylus,
24 PointerDeviceKind.trackpad,
25 // The VoiceAccess sends pointer events with unknown type when scrolling
26 // scrollables.
27 PointerDeviceKind.unknown,
28};
29
30/// Types of overscroll indicators supported by [TargetPlatform.android].
31enum AndroidOverscrollIndicator {
32 /// Utilizes a [StretchingOverscrollIndicator], which transforms the contents
33 /// of a [ScrollView] when overscrolled.
34 stretch,
35
36 /// Utilizes a [GlowingOverscrollIndicator], painting a glowing semi circle on
37 /// top of the [ScrollView] in response to overscrolling.
38 glow,
39}
40
41/// Describes how [Scrollable] widgets should behave.
42///
43/// {@template flutter.widgets.scrollBehavior}
44/// Used by [ScrollConfiguration] to configure the [Scrollable] widgets in a
45/// subtree.
46///
47/// This class can be extended to further customize a [ScrollBehavior] for a
48/// subtree. For example, overriding [ScrollBehavior.getScrollPhysics] sets the
49/// default [ScrollPhysics] for [Scrollable]s that inherit this [ScrollConfiguration].
50/// Overriding [ScrollBehavior.buildOverscrollIndicator] can be used to add or change
51/// the default [GlowingOverscrollIndicator] decoration, while
52/// [ScrollBehavior.buildScrollbar] can be changed to modify the default [Scrollbar].
53///
54/// When looking to easily toggle the default decorations, you can use
55/// [ScrollBehavior.copyWith] instead of creating your own [ScrollBehavior] class.
56/// The `scrollbar` and `overscrollIndicator` flags can turn these decorations off.
57/// {@endtemplate}
58///
59/// See also:
60///
61/// * [ScrollConfiguration], the inherited widget that controls how
62/// [Scrollable] widgets behave in a subtree.
63@immutable
64class ScrollBehavior {
65 /// Creates a description of how [Scrollable] widgets should behave.
66 const ScrollBehavior();
67
68 /// Creates a copy of this ScrollBehavior, making it possible to
69 /// easily toggle `scrollbar` and `overscrollIndicator` effects.
70 ///
71 /// This is used by widgets like [PageView] and [ListWheelScrollView] to
72 /// override the current [ScrollBehavior] and manage how they are decorated.
73 /// Widgets such as these have the option to provide a [ScrollBehavior] on
74 /// the widget level, like [PageView.scrollBehavior], in order to change the
75 /// default.
76 ScrollBehavior copyWith({
77 bool? scrollbars,
78 bool? overscroll,
79 Set<PointerDeviceKind>? dragDevices,
80 MultitouchDragStrategy? multitouchDragStrategy,
81 Set<LogicalKeyboardKey>? pointerAxisModifiers,
82 ScrollPhysics? physics,
83 TargetPlatform? platform,
84 }) {
85 return _WrappedScrollBehavior(
86 delegate: this,
87 scrollbars: scrollbars ?? true,
88 overscroll: overscroll ?? true,
89 dragDevices: dragDevices,
90 multitouchDragStrategy: multitouchDragStrategy,
91 pointerAxisModifiers: pointerAxisModifiers,
92 physics: physics,
93 platform: platform,
94 );
95 }
96
97 /// The platform whose scroll physics should be implemented.
98 ///
99 /// Defaults to the current platform.
100 TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
101
102 /// The device kinds that the scrollable will accept drag gestures from.
103 ///
104 /// By default only [PointerDeviceKind.touch], [PointerDeviceKind.stylus], and
105 /// [PointerDeviceKind.invertedStylus] are configured to create drag gestures.
106 /// Enabling this for [PointerDeviceKind.mouse] will make it difficult or
107 /// impossible to select text in scrollable containers and is not recommended.
108 Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;
109
110 /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.multitouchDragStrategy}
111 ///
112 /// By default, [MultitouchDragStrategy.latestPointer] is configured to
113 /// create drag gestures for all platforms.
114 MultitouchDragStrategy get multitouchDragStrategy => MultitouchDragStrategy.latestPointer;
115
116 /// A set of [LogicalKeyboardKey]s that, when any or all are pressed in
117 /// combination with a [PointerDeviceKind.mouse] pointer scroll event, will
118 /// flip the axes of the scroll input.
119 ///
120 /// This will for example, result in the input of a vertical mouse wheel, to
121 /// move the [ScrollPosition] of a [ScrollView] with an [Axis.horizontal]
122 /// scroll direction.
123 ///
124 /// If other keys exclusive of this set are pressed during a scroll event, in
125 /// conjunction with keys from this set, the scroll input will still be
126 /// flipped.
127 ///
128 /// Defaults to [LogicalKeyboardKey.shiftLeft],
129 /// [LogicalKeyboardKey.shiftRight].
130 Set<LogicalKeyboardKey> get pointerAxisModifiers => <LogicalKeyboardKey>{
131 LogicalKeyboardKey.shiftLeft,
132 LogicalKeyboardKey.shiftRight,
133 };
134
135 /// Applies a [RawScrollbar] to the child widget on desktop platforms.
136 Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
137 // When modifying this function, consider modifying the implementation in
138 // the Material and Cupertino subclasses as well.
139 switch (getPlatform(context)) {
140 case TargetPlatform.linux:
141 case TargetPlatform.macOS:
142 case TargetPlatform.windows:
143 assert(details.controller != null);
144 return RawScrollbar(
145 controller: details.controller,
146 child: child,
147 );
148 case TargetPlatform.android:
149 case TargetPlatform.fuchsia:
150 case TargetPlatform.iOS:
151 return child;
152 }
153 }
154
155 /// Applies a [GlowingOverscrollIndicator] to the child widget on
156 /// [TargetPlatform.android] and [TargetPlatform.fuchsia].
157 Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
158 // When modifying this function, consider modifying the implementation in
159 // the Material and Cupertino subclasses as well.
160 switch (getPlatform(context)) {
161 case TargetPlatform.iOS:
162 case TargetPlatform.linux:
163 case TargetPlatform.macOS:
164 case TargetPlatform.windows:
165 return child;
166 case TargetPlatform.android:
167 case TargetPlatform.fuchsia:
168 return GlowingOverscrollIndicator(
169 axisDirection: details.direction,
170 color: _kDefaultGlowColor,
171 child: child,
172 );
173 }
174 }
175
176 /// Specifies the type of velocity tracker to use in the descendant
177 /// [Scrollable]s' drag gesture recognizers, for estimating the velocity of a
178 /// drag gesture.
179 ///
180 /// This can be used to, for example, apply different fling velocity
181 /// estimation methods on different platforms, in order to match the
182 /// platform's native behavior.
183 ///
184 /// Typically, the provided [GestureVelocityTrackerBuilder] should return a
185 /// fresh velocity tracker. If null is returned, [Scrollable] creates a new
186 /// [VelocityTracker] to track the newly added pointer that may develop into
187 /// a drag gesture.
188 ///
189 /// The default implementation provides a new
190 /// [IOSScrollViewFlingVelocityTracker] on iOS and macOS for each new pointer,
191 /// and a new [VelocityTracker] on other platforms for each new pointer.
192 GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
193 switch (getPlatform(context)) {
194 case TargetPlatform.iOS:
195 return (PointerEvent event) => IOSScrollViewFlingVelocityTracker(event.kind);
196 case TargetPlatform.macOS:
197 return (PointerEvent event) => MacOSScrollViewFlingVelocityTracker(event.kind);
198 case TargetPlatform.android:
199 case TargetPlatform.fuchsia:
200 case TargetPlatform.linux:
201 case TargetPlatform.windows:
202 return (PointerEvent event) => VelocityTracker.withKind(event.kind);
203 }
204 }
205
206 static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
207 static const ScrollPhysics _bouncingDesktopPhysics = BouncingScrollPhysics(
208 decelerationRate: ScrollDecelerationRate.fast,
209 parent: RangeMaintainingScrollPhysics()
210 );
211 static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());
212
213 /// The scroll physics to use for the platform given by [getPlatform].
214 ///
215 /// Defaults to [RangeMaintainingScrollPhysics] mixed with
216 /// [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
217 /// Android.
218 ScrollPhysics getScrollPhysics(BuildContext context) {
219 // When modifying this function, consider modifying the implementation in
220 // the Material and Cupertino subclasses as well.
221 switch (getPlatform(context)) {
222 case TargetPlatform.iOS:
223 return _bouncingPhysics;
224 case TargetPlatform.macOS:
225 return _bouncingDesktopPhysics;
226 case TargetPlatform.android:
227 case TargetPlatform.fuchsia:
228 case TargetPlatform.linux:
229 case TargetPlatform.windows:
230 return _clampingPhysics;
231 }
232 }
233
234 /// Called whenever a [ScrollConfiguration] is rebuilt with a new
235 /// [ScrollBehavior] of the same [runtimeType].
236 ///
237 /// If the new instance represents different information than the old
238 /// instance, then the method should return true, otherwise it should return
239 /// false.
240 ///
241 /// If this method returns true, all the widgets that inherit from the
242 /// [ScrollConfiguration] will rebuild using the new [ScrollBehavior]. If this
243 /// method returns false, the rebuilds might be optimized away.
244 bool shouldNotify(covariant ScrollBehavior oldDelegate) => false;
245
246 @override
247 String toString() => objectRuntimeType(this, 'ScrollBehavior');
248}
249
250class _WrappedScrollBehavior implements ScrollBehavior {
251 const _WrappedScrollBehavior({
252 required this.delegate,
253 this.scrollbars = true,
254 this.overscroll = true,
255 Set<PointerDeviceKind>? dragDevices,
256 MultitouchDragStrategy? multitouchDragStrategy,
257 Set<LogicalKeyboardKey>? pointerAxisModifiers,
258 this.physics,
259 this.platform,
260 }) : _dragDevices = dragDevices,
261 _multitouchDragStrategy = multitouchDragStrategy,
262 _pointerAxisModifiers = pointerAxisModifiers;
263
264 final ScrollBehavior delegate;
265 final bool scrollbars;
266 final bool overscroll;
267 final ScrollPhysics? physics;
268 final TargetPlatform? platform;
269 final Set<PointerDeviceKind>? _dragDevices;
270 final MultitouchDragStrategy? _multitouchDragStrategy;
271 final Set<LogicalKeyboardKey>? _pointerAxisModifiers;
272
273 @override
274 Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
275
276 @override
277 MultitouchDragStrategy get multitouchDragStrategy => _multitouchDragStrategy ?? delegate.multitouchDragStrategy;
278
279 @override
280 Set<LogicalKeyboardKey> get pointerAxisModifiers => _pointerAxisModifiers ?? delegate.pointerAxisModifiers;
281
282 @override
283 Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
284 if (overscroll) {
285 return delegate.buildOverscrollIndicator(context, child, details);
286 }
287 return child;
288 }
289
290 @override
291 Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
292 if (scrollbars) {
293 return delegate.buildScrollbar(context, child, details);
294 }
295 return child;
296 }
297
298 @override
299 ScrollBehavior copyWith({
300 bool? scrollbars,
301 bool? overscroll,
302 Set<PointerDeviceKind>? dragDevices,
303 MultitouchDragStrategy? multitouchDragStrategy,
304 Set<LogicalKeyboardKey>? pointerAxisModifiers,
305 ScrollPhysics? physics,
306 TargetPlatform? platform,
307 }) {
308 return delegate.copyWith(
309 scrollbars: scrollbars ?? this.scrollbars,
310 overscroll: overscroll ?? this.overscroll,
311 dragDevices: dragDevices ?? this.dragDevices,
312 multitouchDragStrategy: multitouchDragStrategy ?? this.multitouchDragStrategy,
313 pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers,
314 physics: physics ?? this.physics,
315 platform: platform ?? this.platform,
316 );
317 }
318
319 @override
320 TargetPlatform getPlatform(BuildContext context) {
321 return platform ?? delegate.getPlatform(context);
322 }
323
324 @override
325 ScrollPhysics getScrollPhysics(BuildContext context) {
326 return physics ?? delegate.getScrollPhysics(context);
327 }
328
329 @override
330 bool shouldNotify(_WrappedScrollBehavior oldDelegate) {
331 return oldDelegate.delegate.runtimeType != delegate.runtimeType
332 || oldDelegate.scrollbars != scrollbars
333 || oldDelegate.overscroll != overscroll
334 || !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
335 || oldDelegate.multitouchDragStrategy != multitouchDragStrategy
336 || !setEquals<LogicalKeyboardKey>(oldDelegate.pointerAxisModifiers, pointerAxisModifiers)
337 || oldDelegate.physics != physics
338 || oldDelegate.platform != platform
339 || delegate.shouldNotify(oldDelegate.delegate);
340 }
341
342 @override
343 GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
344 return delegate.velocityTrackerBuilder(context);
345 }
346
347 @override
348 String toString() => objectRuntimeType(this, '_WrappedScrollBehavior');
349}
350
351/// Controls how [Scrollable] widgets behave in a subtree.
352///
353/// The scroll configuration determines the [ScrollPhysics] and viewport
354/// decorations used by descendants of [child].
355class ScrollConfiguration extends InheritedWidget {
356 /// Creates a widget that controls how [Scrollable] widgets behave in a subtree.
357 const ScrollConfiguration({
358 super.key,
359 required this.behavior,
360 required super.child,
361 });
362
363 /// How [Scrollable] widgets that are descendants of [child] should behave.
364 final ScrollBehavior behavior;
365
366 /// The [ScrollBehavior] for [Scrollable] widgets in the given [BuildContext].
367 ///
368 /// If no [ScrollConfiguration] widget is in scope of the given `context`,
369 /// a default [ScrollBehavior] instance is returned.
370 static ScrollBehavior of(BuildContext context) {
371 final ScrollConfiguration? configuration = context.dependOnInheritedWidgetOfExactType<ScrollConfiguration>();
372 return configuration?.behavior ?? const ScrollBehavior();
373 }
374
375 @override
376 bool updateShouldNotify(ScrollConfiguration oldWidget) {
377 return behavior.runtimeType != oldWidget.behavior.runtimeType
378 || (behavior != oldWidget.behavior && behavior.shouldNotify(oldWidget.behavior));
379 }
380
381 @override
382 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
383 super.debugFillProperties(properties);
384 properties.add(DiagnosticsProperty<ScrollBehavior>('behavior', behavior));
385 }
386}
387