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

import 'package:flutter/foundation.dart';
6
import 'package:flutter/painting.dart';
7 8

import 'framework.dart';
9
import 'scroll_configuration.dart';
10 11
import 'scroll_controller.dart';

12 13 14 15 16 17
const Set<TargetPlatform> _kMobilePlatforms = <TargetPlatform>{
  TargetPlatform.android,
  TargetPlatform.iOS,
  TargetPlatform.fuchsia,
};

18 19
/// Associates a [ScrollController] with a subtree.
///
20 21
/// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU}
///
22 23 24 25 26
/// When a [ScrollView] has [ScrollView.primary] set to true, the [ScrollView]
/// uses [of] to inherit the [PrimaryScrollController] associated with its
/// subtree.
///
/// A ScrollView that doesn't have a controller or the primary flag set will
27
/// inherit the PrimaryScrollController, if [shouldInherit] allows it. By
28 29 30
/// default [shouldInherit] is true for mobile platforms when the ScrollView has
/// a scroll direction of [Axis.vertical]. This automatic inheritance can be
/// configured with [automaticallyInheritForPlatforms] and [scrollDirection].
31
///
32 33 34
/// Inheriting this ScrollController can provide default behavior for scroll
/// views in a subtree. For example, the [Scaffold] uses this mechanism to
/// implement the scroll-to-top gesture on iOS.
35 36 37 38 39 40 41 42 43 44 45 46 47
///
/// Another default behavior handled by the PrimaryScrollController is default
/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
/// part of the application, the ScrollAction will be evaluated using the scroll
/// view associated with a PrimaryScrollController, for example, when executing
/// [Shortcuts] key events like page up and down.
///
/// See also:
///   * [ScrollAction], an [Action] that scrolls the [Scrollable] that encloses
///     the current [primaryFocus] or is attached to the PrimaryScrollController.
///   * [Shortcuts], a widget that establishes a [ShortcutManager] to be used
///     by its descendants when invoking an [Action] via a keyboard key
///     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 54
    this.automaticallyInheritForPlatforms = _kMobilePlatforms,
    this.scrollDirection = Axis.vertical,
55
    required super.child,
56
  });
57

58
  /// Creates a subtree without an associated [ScrollController].
59
  const PrimaryScrollController.none({
60 61
    super.key,
    required super.child,
62 63 64
  }) : automaticallyInheritForPlatforms = const <TargetPlatform>{},
       scrollDirection = null,
       controller = null;
65

66
  /// The [ScrollController] associated with the subtree.
67 68 69 70 71
  ///
  /// See also:
  ///
  ///  * [ScrollView.controller], which discusses the purpose of specifying a
  ///    scroll controller.
72
  final ScrollController? controller;
73

74 75 76 77 78 79 80 81 82 83 84 85 86
  /// The [Axis] this controller is configured for [ScrollView]s to
  /// automatically inherit.
  ///
  /// Used in conjunction with [automaticallyInheritForPlatforms]. If the
  /// current [TargetPlatform] is not included in
  /// [automaticallyInheritForPlatforms], this is ignored.
  ///
  /// When null, no [ScrollView] in any Axis will automatically inherit this
  /// controller. This is dissimilar to [PrimaryScrollController.none]. When a
  /// PrimaryScrollController is inherited, ScrollView will insert
  /// PrimaryScrollController.none into the tree to prevent further descendant
  /// ScrollViews from inheriting the current PrimaryScrollController.
  ///
87 88 89
  /// For the direction in which active scrolling may be occurring, see
  /// [ScrollDirection].
  ///
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
  /// Defaults to [Axis.vertical].
  final Axis? scrollDirection;

  /// The [TargetPlatform]s this controller is configured for [ScrollView]s to
  /// automatically inherit.
  ///
  /// Used in conjunction with [scrollDirection]. If the [Axis] provided to
  /// [shouldInherit] is not [scrollDirection], this is ignored.
  ///
  /// When empty, no ScrollView in any Axis will automatically inherit this
  /// controller. Defaults to [TargetPlatformVariant.mobile].
  final Set<TargetPlatform> automaticallyInheritForPlatforms;

  /// Returns true if this PrimaryScrollController is configured to be
  /// automatically inherited for the current [TargetPlatform] and the given
  /// [Axis].
  ///
  /// This method is typically not called directly. [ScrollView] will call this
  /// method if it has not been provided a [ScrollController] and
  /// [ScrollView.primary] is unset.
  ///
  /// If a ScrollController has already been provided to
  /// [ScrollView.controller], or [ScrollView.primary] is set, this is method is
  /// not called by ScrollView as it will have determined whether or not to
  /// inherit the PrimaryScrollController.
  static bool shouldInherit(BuildContext context, Axis scrollDirection) {
    final PrimaryScrollController? result = context.findAncestorWidgetOfExactType<PrimaryScrollController>();
    if (result == null) {
      return false;
    }

    final TargetPlatform platform = ScrollConfiguration.of(context).getPlatform(context);
    if (result.automaticallyInheritForPlatforms.contains(platform)) {
      return result.scrollDirection == scrollDirection;
    }
    return false;
  }

128 129 130 131 132
  /// Returns the [ScrollController] most closely associated with the given
  /// context.
  ///
  /// Returns null if there is no [ScrollController] associated with the given
  /// context.
133 134 135 136 137 138 139 140 141
  ///
  /// Calling this method will create a dependency on the closest
  /// [PrimaryScrollController] in the [context], if there is one.
  ///
  /// See also:
  ///
  /// * [PrimaryScrollController.maybeOf], which is similar to this method, but
  ///   asserts if no [PrimaryScrollController] ancestor is found.
  static ScrollController? maybeOf(BuildContext context) {
142
    final PrimaryScrollController? result = context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
143 144 145
    return result?.controller;
  }

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
  /// Returns the [ScrollController] most closely associated with the given
  /// context.
  ///
  /// If no ancestor is found, this method will assert in debug mode, and throw
  /// an exception in release mode.
  ///
  /// Calling this method will create a dependency on the closest
  /// [PrimaryScrollController] in the [context].
  ///
  /// See also:
  ///
  /// * [PrimaryScrollController.maybeOf], which is similar to this method, but
  ///   returns null if no [PrimaryScrollController] ancestor is found.
  static ScrollController of(BuildContext context) {
    final ScrollController? controller = maybeOf(context);
    assert(() {
      if (controller == null) {
        throw FlutterError(
          'PrimaryScrollController.of() was called with a context that does not contain a '
          'PrimaryScrollController widget.\n'
          'No PrimaryScrollController widget ancestor could be found starting from the '
          'context that was passed to PrimaryScrollController.of(). This can happen '
          'because you are using a widget that looks for a PrimaryScrollController '
          'ancestor, but no such ancestor exists.\n'
          'The context used was:\n'
          '  $context',
        );
      }
      return true;
    }());
    return controller!;
  }

179
  @override
180
  bool updateShouldNotify(PrimaryScrollController oldWidget) => controller != oldWidget.controller;
181 182

  @override
183 184
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
185
    properties.add(DiagnosticsProperty<ScrollController>('controller', controller, ifNull: 'no controller', showName: false));
186 187
  }
}