blob: dea998277de273e5c311ade958f4cd2b6a4d2bb6 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// 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';
import 'package:flutter/painting.dart';
import 'framework.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
const Set<TargetPlatform> _kMobilePlatforms = <TargetPlatform>{
TargetPlatform.android,
TargetPlatform.iOS,
TargetPlatform.fuchsia,
};
/// Associates a [ScrollController] with a subtree.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU}
///
/// 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
/// inherit the PrimaryScrollController, if [shouldInherit] allows it. By
/// 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].
///
/// 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.
///
/// 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].
class PrimaryScrollController extends InheritedWidget {
/// Creates a widget that associates a [ScrollController] with a subtree.
const PrimaryScrollController({
super.key,
required ScrollController this.controller,
this.automaticallyInheritForPlatforms = _kMobilePlatforms,
this.scrollDirection = Axis.vertical,
required super.child,
});
/// Creates a subtree without an associated [ScrollController].
const PrimaryScrollController.none({
super.key,
required super.child,
}) : automaticallyInheritForPlatforms = const <TargetPlatform>{},
scrollDirection = null,
controller = null;
/// The [ScrollController] associated with the subtree.
///
/// See also:
///
/// * [ScrollView.controller], which discusses the purpose of specifying a
/// scroll controller.
final ScrollController? controller;
/// 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.
///
/// For the direction in which active scrolling may be occurring, see
/// [ScrollDirection].
///
/// 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;
}
/// Returns the [ScrollController] most closely associated with the given
/// context.
///
/// Returns null if there is no [ScrollController] associated with the given
/// context.
///
/// 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) {
final PrimaryScrollController? result = context.dependOnInheritedWidgetOfExactType<PrimaryScrollController>();
return result?.controller;
}
/// 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!;
}
@override
bool updateShouldNotify(PrimaryScrollController oldWidget) => controller != oldWidget.controller;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ScrollController>('controller', controller, ifNull: 'no controller', showName: false));
}
}