scroll_configuration.dart 14.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/gestures.dart';
7
import 'package:flutter/rendering.dart';
8

9
import 'framework.dart';
10
import 'overscroll_indicator.dart';
11
import 'scroll_physics.dart';
12 13
import 'scrollable.dart';
import 'scrollbar.dart';
14

15
const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
16

17 18 19 20 21
/// Device types that scrollables should accept drag gestures from by default.
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
  PointerDeviceKind.touch,
  PointerDeviceKind.stylus,
  PointerDeviceKind.invertedStylus,
22
  PointerDeviceKind.trackpad,
23 24 25
  // The VoiceAccess sends pointer events with unknown type when scrolling
  // scrollables.
  PointerDeviceKind.unknown,
26 27
};

28 29 30 31 32 33 34 35 36 37 38
/// The default overscroll indicator applied on [TargetPlatform.android].
// TODO(Piinks): Complete migration to stretch by default.
const AndroidOverscrollIndicator _kDefaultAndroidOverscrollIndicator = AndroidOverscrollIndicator.glow;

/// Types of overscroll indicators supported by [TargetPlatform.android].
enum AndroidOverscrollIndicator {
  /// Utilizes a [StretchingOverscrollIndicator], which transforms the contents
  /// of a [ScrollView] when overscrolled.
  stretch,

  /// Utilizes a [GlowingOverscrollIndicator], painting a glowing semi circle on
39
  /// top of the [ScrollView] in response to overscrolling.
40 41 42
  glow,
}

43 44
/// Describes how [Scrollable] widgets should behave.
///
45
/// {@template flutter.widgets.scrollBehavior}
46 47
/// Used by [ScrollConfiguration] to configure the [Scrollable] widgets in a
/// subtree.
48 49 50 51
///
/// This class can be extended to further customize a [ScrollBehavior] for a
/// subtree. For example, overriding [ScrollBehavior.getScrollPhysics] sets the
/// default [ScrollPhysics] for [Scrollable]s that inherit this [ScrollConfiguration].
52 53 54 55 56 57 58
/// Overriding [ScrollBehavior.buildOverscrollIndicator] can be used to add or change
/// the default [GlowingOverscrollIndicator] decoration, while
/// [ScrollBehavior.buildScrollbar] can be changed to modify the default [Scrollbar].
///
/// When looking to easily toggle the default decorations, you can use
/// [ScrollBehavior.copyWith] instead of creating your own [ScrollBehavior] class.
/// The `scrollbar` and `overscrollIndicator` flags can turn these decorations off.
59 60 61 62 63 64
/// {@endtemplate}
///
/// See also:
///
///   * [ScrollConfiguration], the inherited widget that controls how
///     [Scrollable] widgets behave in a subtree.
65
@immutable
Adam Barth's avatar
Adam Barth committed
66
class ScrollBehavior {
67
  /// Creates a description of how [Scrollable] widgets should behave.
68
  const ScrollBehavior({
69 70 71 72
    @Deprecated(
      'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. '
      'This feature was deprecated after v2.13.0-0.0.pre.'
    )
73 74 75
    AndroidOverscrollIndicator? androidOverscrollIndicator,
  }): _androidOverscrollIndicator = androidOverscrollIndicator;

76 77 78 79 80 81 82 83
  /// Specifies which overscroll indicator to use on [TargetPlatform.android].
  ///
  /// Cannot be null. Defaults to [AndroidOverscrollIndicator.glow].
  ///
  /// See also:
  ///
  ///   * [MaterialScrollBehavior], which supports setting this property
  ///     using [ThemeData].
84 85 86 87
  @Deprecated(
    'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. '
    'This feature was deprecated after v2.13.0-0.0.pre.'
  )
88 89
  AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? _kDefaultAndroidOverscrollIndicator;
  final AndroidOverscrollIndicator? _androidOverscrollIndicator;
90

91 92 93 94 95 96 97 98 99
  /// Creates a copy of this ScrollBehavior, making it possible to
  /// easily toggle `scrollbar` and `overscrollIndicator` effects.
  ///
  /// This is used by widgets like [PageView] and [ListWheelScrollView] to
  /// override the current [ScrollBehavior] and manage how they are decorated.
  /// Widgets such as these have the option to provide a [ScrollBehavior] on
  /// the widget level, like [PageView.scrollBehavior], in order to change the
  /// default.
  ScrollBehavior copyWith({
100 101
    bool? scrollbars,
    bool? overscroll,
102
    Set<PointerDeviceKind>? dragDevices,
103 104
    ScrollPhysics? physics,
    TargetPlatform? platform,
105 106 107 108
    @Deprecated(
      'Use ThemeData.useMaterial3 or override ScrollBehavior.buildOverscrollIndicator. '
      'This feature was deprecated after v2.13.0-0.0.pre.'
    )
109
    AndroidOverscrollIndicator? androidOverscrollIndicator,
110 111 112
  }) {
    return _WrappedScrollBehavior(
      delegate: this,
113 114
      scrollbars: scrollbars ?? true,
      overscroll: overscroll ?? true,
115 116
      physics: physics,
      platform: platform,
117
      dragDevices: dragDevices,
118
      androidOverscrollIndicator: androidOverscrollIndicator
119 120 121
    );
  }

122 123 124 125 126
  /// The platform whose scroll physics should be implemented.
  ///
  /// Defaults to the current platform.
  TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;

127 128 129 130 131 132 133 134
  /// The device kinds that the scrollable will accept drag gestures from.
  ///
  /// By default only [PointerDeviceKind.touch], [PointerDeviceKind.stylus], and
  /// [PointerDeviceKind.invertedStylus] are configured to create drag gestures.
  /// Enabling this for [PointerDeviceKind.mouse] will make it difficult or
  /// impossible to select text in scrollable containers and is not recommended.
  Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;

135 136 137 138 139 140 141 142 143 144
  /// Applies a [RawScrollbar] to the child widget on desktop platforms.
  Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
    // When modifying this function, consider modifying the implementation in
    // the Material and Cupertino subclasses as well.
    switch (getPlatform(context)) {
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return RawScrollbar(
          controller: details.controller,
145
          child: child,
146
        );
147 148 149 150
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.iOS:
        return child;
151 152 153
    }
  }

154 155 156 157 158
  /// Applies a [GlowingOverscrollIndicator] to the child widget on
  /// [TargetPlatform.android] and [TargetPlatform.fuchsia].
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
    // When modifying this function, consider modifying the implementation in
    // the Material and Cupertino subclasses as well.
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return child;
      case TargetPlatform.android:
        switch (androidOverscrollIndicator) {
          case AndroidOverscrollIndicator.stretch:
            return StretchingOverscrollIndicator(
              axisDirection: details.direction,
              child: child,
            );
          case AndroidOverscrollIndicator.glow:
            continue glow;
        }
      glow:
      case TargetPlatform.fuchsia:
        return GlowingOverscrollIndicator(
          axisDirection: details.direction,
          color: _kDefaultGlowColor,
          child: child,
        );
    }
183 184
  }

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
  /// Specifies the type of velocity tracker to use in the descendant
  /// [Scrollable]s' drag gesture recognizers, for estimating the velocity of a
  /// drag gesture.
  ///
  /// This can be used to, for example, apply different fling velocity
  /// estimation methods on different platforms, in order to match the
  /// platform's native behavior.
  ///
  /// Typically, the provided [GestureVelocityTrackerBuilder] should return a
  /// fresh velocity tracker. If null is returned, [Scrollable] creates a new
  /// [VelocityTracker] to track the newly added pointer that may develop into
  /// a drag gesture.
  ///
  /// The default implementation provides a new
  /// [IOSScrollViewFlingVelocityTracker] on iOS and macOS for each new pointer,
  /// and a new [VelocityTracker] on other platforms for each new pointer.
  GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
204
        return (PointerEvent event) => IOSScrollViewFlingVelocityTracker(event.kind);
205 206
      case TargetPlatform.macOS:
        return (PointerEvent event) => MacOSScrollViewFlingVelocityTracker(event.kind);
207 208 209 210
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
211
        return (PointerEvent event) => VelocityTracker.withKind(event.kind);
212 213 214
    }
  }

215
  static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
216 217 218 219
  static const ScrollPhysics _bouncingDesktopPhysics = BouncingScrollPhysics(
    decelerationRate: ScrollDecelerationRate.fast,
    parent: RangeMaintainingScrollPhysics()
  );
220 221
  static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());

222
  /// The scroll physics to use for the platform given by [getPlatform].
223
  ///
224 225
  /// Defaults to [RangeMaintainingScrollPhysics] mixed with
  /// [BouncingScrollPhysics] on iOS and [ClampingScrollPhysics] on
226
  /// Android.
227
  ScrollPhysics getScrollPhysics(BuildContext context) {
228 229
    // When modifying this function, consider modifying the implementation in
    // the Material and Cupertino subclasses as well.
230 231
    switch (getPlatform(context)) {
      case TargetPlatform.iOS:
232
        return _bouncingPhysics;
233 234
      case TargetPlatform.macOS:
        return _bouncingDesktopPhysics;
235 236
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
237 238
      case TargetPlatform.linux:
      case TargetPlatform.windows:
239
        return _clampingPhysics;
240 241 242
    }
  }

Adam Barth's avatar
Adam Barth committed
243 244 245 246 247 248 249 250 251 252
  /// Called whenever a [ScrollConfiguration] is rebuilt with a new
  /// [ScrollBehavior] of the same [runtimeType].
  ///
  /// If the new instance represents different information than the old
  /// instance, then the method should return true, otherwise it should return
  /// false.
  ///
  /// If this method returns true, all the widgets that inherit from the
  /// [ScrollConfiguration] will rebuild using the new [ScrollBehavior]. If this
  /// method returns false, the rebuilds might be optimized away.
253
  bool shouldNotify(covariant ScrollBehavior oldDelegate) => false;
254 255

  @override
256
  String toString() => objectRuntimeType(this, 'ScrollBehavior');
257 258
}

259 260 261
class _WrappedScrollBehavior implements ScrollBehavior {
  const _WrappedScrollBehavior({
    required this.delegate,
262 263
    this.scrollbars = true,
    this.overscroll = true,
264 265
    this.physics,
    this.platform,
266
    Set<PointerDeviceKind>? dragDevices,
267 268 269
    AndroidOverscrollIndicator? androidOverscrollIndicator,
  }) : _androidOverscrollIndicator = androidOverscrollIndicator,
       _dragDevices = dragDevices;
270 271

  final ScrollBehavior delegate;
272 273
  final bool scrollbars;
  final bool overscroll;
274 275
  final ScrollPhysics? physics;
  final TargetPlatform? platform;
276
  final Set<PointerDeviceKind>? _dragDevices;
277 278
  @override
  final AndroidOverscrollIndicator? _androidOverscrollIndicator;
279 280 281

  @override
  Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;
282

283
  @override
284
  AndroidOverscrollIndicator get androidOverscrollIndicator => _androidOverscrollIndicator ?? delegate.androidOverscrollIndicator;
285

286 287
  @override
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
288
    if (overscroll) {
289
      return delegate.buildOverscrollIndicator(context, child, details);
290
    }
291 292 293 294 295
    return child;
  }

  @override
  Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
296
    if (scrollbars) {
297
      return delegate.buildScrollbar(context, child, details);
298
    }
299 300 301 302 303
    return child;
  }

  @override
  ScrollBehavior copyWith({
304 305
    bool? scrollbars,
    bool? overscroll,
306 307
    ScrollPhysics? physics,
    TargetPlatform? platform,
308
    Set<PointerDeviceKind>? dragDevices,
309
    AndroidOverscrollIndicator? androidOverscrollIndicator
310 311
  }) {
    return delegate.copyWith(
312 313 314 315 316 317
      scrollbars: scrollbars ?? this.scrollbars,
      overscroll: overscroll ?? this.overscroll,
      physics: physics ?? this.physics,
      platform: platform ?? this.platform,
      dragDevices: dragDevices ?? this.dragDevices,
      androidOverscrollIndicator: androidOverscrollIndicator ?? this.androidOverscrollIndicator,
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
    );
  }

  @override
  TargetPlatform getPlatform(BuildContext context) {
    return platform ?? delegate.getPlatform(context);
  }

  @override
  ScrollPhysics getScrollPhysics(BuildContext context) {
    return physics ?? delegate.getScrollPhysics(context);
  }

  @override
  bool shouldNotify(_WrappedScrollBehavior oldDelegate) {
    return oldDelegate.delegate.runtimeType != delegate.runtimeType
334 335
        || oldDelegate.scrollbars != scrollbars
        || oldDelegate.overscroll != overscroll
336 337
        || oldDelegate.physics != physics
        || oldDelegate.platform != platform
338
        || !setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
339 340 341 342 343 344 345 346 347 348 349 350
        || delegate.shouldNotify(oldDelegate.delegate);
  }

  @override
  GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
    return delegate.velocityTrackerBuilder(context);
  }

  @override
  String toString() => objectRuntimeType(this, '_WrappedScrollBehavior');
}

351 352 353
/// Controls how [Scrollable] widgets behave in a subtree.
///
/// The scroll configuration determines the [ScrollPhysics] and viewport
354
/// decorations used by descendants of [child].
Adam Barth's avatar
Adam Barth committed
355
class ScrollConfiguration extends InheritedWidget {
356 357 358
  /// Creates a widget that controls how [Scrollable] widgets behave in a subtree.
  ///
  /// The [behavior] and [child] arguments must not be null.
Adam Barth's avatar
Adam Barth committed
359
  const ScrollConfiguration({
360
    super.key,
361
    required this.behavior,
362 363
    required super.child,
  });
364

365
  /// How [Scrollable] widgets that are descendants of [child] should behave.
Adam Barth's avatar
Adam Barth committed
366
  final ScrollBehavior behavior;
367

368
  /// The [ScrollBehavior] for [Scrollable] widgets in the given [BuildContext].
369 370 371
  ///
  /// If no [ScrollConfiguration] widget is in scope of the given `context`,
  /// a default [ScrollBehavior] instance is returned.
Adam Barth's avatar
Adam Barth committed
372
  static ScrollBehavior of(BuildContext context) {
373
    final ScrollConfiguration? configuration = context.dependOnInheritedWidgetOfExactType<ScrollConfiguration>();
Adam Barth's avatar
Adam Barth committed
374
    return configuration?.behavior ?? const ScrollBehavior();
375 376 377
  }

  @override
Adam Barth's avatar
Adam Barth committed
378
  bool updateShouldNotify(ScrollConfiguration oldWidget) {
379
    assert(behavior != null);
Adam Barth's avatar
Adam Barth committed
380 381
    return behavior.runtimeType != oldWidget.behavior.runtimeType
        || (behavior != oldWidget.behavior && behavior.shouldNotify(oldWidget.behavior));
382
  }
383 384

  @override
385 386
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
387
    properties.add(DiagnosticsProperty<ScrollBehavior>('behavior', behavior));
388
  }
389
}