primary_scroll_controller.dart 7.48 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 87 88 89 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
  /// 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.
  ///
  /// 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;
  }

125 126 127 128 129
  /// Returns the [ScrollController] most closely associated with the given
  /// context.
  ///
  /// Returns null if there is no [ScrollController] associated with the given
  /// context.
130 131 132 133 134 135 136 137 138
  ///
  /// 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) {
139
    final PrimaryScrollController? result = context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
140 141 142
    return result?.controller;
  }

143 144 145 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
  /// 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!;
  }

176
  @override
177
  bool updateShouldNotify(PrimaryScrollController oldWidget) => controller != oldWidget.controller;
178 179

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