Unverified Commit e40610d6 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Automatically applying Scrollbars on desktop platforms with configurable ScrollBehaviors (#78588)

parent b083bc14
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
...@@ -10,6 +11,7 @@ import 'icons.dart'; ...@@ -10,6 +11,7 @@ import 'icons.dart';
import 'interface_level.dart'; import 'interface_level.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'route.dart'; import 'route.dart';
import 'scrollbar.dart';
import 'theme.dart'; import 'theme.dart';
/// An application that uses Cupertino design. /// An application that uses Cupertino design.
...@@ -422,14 +424,40 @@ class CupertinoApp extends StatefulWidget { ...@@ -422,14 +424,40 @@ class CupertinoApp extends StatefulWidget {
/// Setting a [CupertinoScrollBehavior] will result in descendant [Scrollable] widgets /// Setting a [CupertinoScrollBehavior] will result in descendant [Scrollable] widgets
/// using [BouncingScrollPhysics] by default. No [GlowingOverscrollIndicator] is /// using [BouncingScrollPhysics] by default. No [GlowingOverscrollIndicator] is
/// applied when using a [CupertinoScrollBehavior] either, regardless of platform. /// applied when using a [CupertinoScrollBehavior] either, regardless of platform.
/// When executing on desktop platforms, a [CupertinoScrollbar] is applied to the child.
/// ///
/// See also: /// See also:
/// ///
/// * [ScrollBehavior], the default scrolling behavior extended by this class. /// * [ScrollBehavior], the default scrolling behavior extended by this class.
class CupertinoScrollBehavior extends ScrollBehavior { class CupertinoScrollBehavior extends ScrollBehavior {
/// Creates a CupertinoScrollBehavior that uses [BouncingScrollPhysics] and
/// adds [CupertinoScrollbar]s on desktop platforms.
const CupertinoScrollBehavior();
@override
Widget buildScrollbar(BuildContext context , Widget child, ScrollableDetails details) {
// When modifying this function, consider modifying the implementation in
// the base class as well.
switch (getPlatform(context)) {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return CupertinoScrollbar(
child: child,
controller: details.controller,
);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
return child;
}
}
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
// Never build any overscroll glow indicators. // No overscroll indicator.
// When modifying this function, consider modifying the implementation in
// the base class as well.
return child; return child;
} }
...@@ -544,7 +572,7 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -544,7 +572,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData(); final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
return ScrollConfiguration( return ScrollConfiguration(
behavior: widget.scrollBehavior ?? CupertinoScrollBehavior(), behavior: widget.scrollBehavior ?? const CupertinoScrollBehavior(),
child: CupertinoUserInterfaceLevel( child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.base, data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme( child: CupertinoTheme(
......
...@@ -14,6 +14,7 @@ import 'icons.dart'; ...@@ -14,6 +14,7 @@ import 'icons.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'page.dart'; import 'page.dart';
import 'scaffold.dart' show ScaffoldMessenger, ScaffoldMessengerState; import 'scaffold.dart' show ScaffoldMessenger, ScaffoldMessengerState;
import 'scrollbar.dart';
import 'theme.dart'; import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage /// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
...@@ -684,17 +685,47 @@ class MaterialApp extends StatefulWidget { ...@@ -684,17 +685,47 @@ class MaterialApp extends StatefulWidget {
/// [GlowingOverscrollIndicator] to [Scrollable] descendants when executing on /// [GlowingOverscrollIndicator] to [Scrollable] descendants when executing on
/// [TargetPlatform.android] and [TargetPlatform.fuchsia]. /// [TargetPlatform.android] and [TargetPlatform.fuchsia].
/// ///
/// When using the desktop platform, if the [Scrollable] widget scrolls in the
/// [Axis.vertical], a [Scrollbar] is applied.
///
/// See also: /// See also:
/// ///
/// * [ScrollBehavior], the default scrolling behavior extended by this class. /// * [ScrollBehavior], the default scrolling behavior extended by this class.
class MaterialScrollBehavior extends ScrollBehavior { class MaterialScrollBehavior extends ScrollBehavior {
/// Creates a MaterialScrollBehavior that decorates [Scrollable]s with
/// [GlowingOverscrollIndicator]s and [Scrollbar]s based on the current
/// platform and provided [ScrollableDetails].
const MaterialScrollBehavior();
@override
TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform;
@override @override
TargetPlatform getPlatform(BuildContext context) { Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
return Theme.of(context).platform; // When modifying this function, consider modifying the implementation in
// the base class as well.
switch (axisDirectionToAxis(details.direction)) {
case Axis.horizontal:
return child;
case Axis.vertical:
switch (getPlatform(context)) {
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return Scrollbar(
child: child,
controller: details.controller,
);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
return child;
}
}
} }
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
// When modifying this function, consider modifying the implementation in // When modifying this function, consider modifying the implementation in
// the base class as well. // the base class as well.
switch (getPlatform(context)) { switch (getPlatform(context)) {
...@@ -707,7 +738,7 @@ class MaterialScrollBehavior extends ScrollBehavior { ...@@ -707,7 +738,7 @@ class MaterialScrollBehavior extends ScrollBehavior {
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
child: child, child: child,
axisDirection: axisDirection, axisDirection: details.direction,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
); );
} }
...@@ -880,7 +911,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -880,7 +911,7 @@ class _MaterialAppState extends State<MaterialApp> {
}()); }());
return ScrollConfiguration( return ScrollConfiguration(
behavior: widget.scrollBehavior ?? MaterialScrollBehavior(), behavior: widget.scrollBehavior ?? const MaterialScrollBehavior(),
child: HeroControllerScope( child: HeroControllerScope(
controller: _heroController, controller: _heroController,
child: result, child: result,
......
...@@ -88,21 +88,6 @@ class _DropdownMenuPainter extends CustomPainter { ...@@ -88,21 +88,6 @@ class _DropdownMenuPainter extends CustomPainter {
} }
} }
// Do not use the platform-specific default scroll configuration.
// Dropdown menus should never overscroll or display an overscroll indicator.
class _DropdownScrollBehavior extends ScrollBehavior {
const _DropdownScrollBehavior();
@override
TargetPlatform getPlatform(BuildContext context) => Theme.of(context).platform;
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override
ScrollPhysics getScrollPhysics(BuildContext context) => const ClampingScrollPhysics();
}
// The widget that is the button wrapping the menu items. // The widget that is the button wrapping the menu items.
class _DropdownMenuItemButton<T> extends StatefulWidget { class _DropdownMenuItemButton<T> extends StatefulWidget {
const _DropdownMenuItemButton({ const _DropdownMenuItemButton({
...@@ -289,7 +274,14 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> { ...@@ -289,7 +274,14 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: route.style, textStyle: route.style,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: const _DropdownScrollBehavior(), // Dropdown menus should never overscroll or display an overscroll indicator.
// The default scrollbar platforms will apply.
// Platform must use Theme and ScrollPhysics must be Clamping.
behavior: ScrollConfiguration.of(context).copyWith(
overscroll: false,
physics: const ClampingScrollPhysics(),
platform: Theme.of(context).platform,
),
child: PrimaryScrollController( child: PrimaryScrollController(
controller: widget.route.scrollController!, controller: widget.route.scrollController!,
child: Scrollbar( child: Scrollbar(
......
...@@ -23,6 +23,7 @@ import 'focus_scope.dart'; ...@@ -23,6 +23,7 @@ import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
...@@ -493,6 +494,7 @@ class EditableText extends StatefulWidget { ...@@ -493,6 +494,7 @@ class EditableText extends StatefulWidget {
this.autofillHints, this.autofillHints,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId, this.restorationId,
this.scrollBehavior,
}) : assert(controller != null), }) : assert(controller != null),
assert(focusNode != null), assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscuringCharacter != null && obscuringCharacter.length == 1),
...@@ -1201,6 +1203,10 @@ class EditableText extends StatefulWidget { ...@@ -1201,6 +1203,10 @@ class EditableText extends StatefulWidget {
/// ///
/// See [Scrollable.physics]. /// See [Scrollable.physics].
/// {@endtemplate} /// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [scrollPhysics].
final ScrollPhysics? scrollPhysics; final ScrollPhysics? scrollPhysics;
/// {@template flutter.widgets.editableText.selectionEnabled} /// {@template flutter.widgets.editableText.selectionEnabled}
...@@ -1305,6 +1311,23 @@ class EditableText extends StatefulWidget { ...@@ -1305,6 +1311,23 @@ class EditableText extends StatefulWidget {
/// Flutter. /// Flutter.
final String? restorationId; final String? restorationId;
/// {@template flutter.widgets.shadow.scrollBehavior}
/// A [ScrollBehavior] that will be applied to this widget individually.
///
/// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
/// modified to alter the viewport decoration, like [Scrollbar]s.
/// {@endtemplate}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to only apply a [Scrollbar] if [maxLines] is greater
/// than 1.
final ScrollBehavior? scrollBehavior;
// Infer the keyboard type of an `EditableText` if it's not specified. // Infer the keyboard type of an `EditableText` if it's not specified.
static TextInputType _inferKeyboardType({ static TextInputType _inferKeyboardType({
required Iterable<String>? autofillHints, required Iterable<String>? autofillHints,
...@@ -2609,6 +2632,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2609,6 +2632,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
restorationId: widget.restorationId, restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ??
// Remove scrollbars if only single line
(_isMultiline ? null : ScrollConfiguration.of(context).copyWith(scrollbars: false)),
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return CompositedTransformTarget( return CompositedTransformTarget(
link: _toolbarLayerLink, link: _toolbarLayerLink,
......
...@@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart'; import 'scroll_context.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_metrics.dart'; import 'scroll_metrics.dart';
...@@ -431,6 +432,7 @@ class _FixedExtentScrollable extends Scrollable { ...@@ -431,6 +432,7 @@ class _FixedExtentScrollable extends Scrollable {
required this.itemExtent, required this.itemExtent,
required ViewportBuilder viewportBuilder, required ViewportBuilder viewportBuilder,
String? restorationId, String? restorationId,
ScrollBehavior? scrollBehavior,
}) : super ( }) : super (
key: key, key: key,
axisDirection: axisDirection, axisDirection: axisDirection,
...@@ -438,6 +440,7 @@ class _FixedExtentScrollable extends Scrollable { ...@@ -438,6 +440,7 @@ class _FixedExtentScrollable extends Scrollable {
physics: physics, physics: physics,
viewportBuilder: viewportBuilder, viewportBuilder: viewportBuilder,
restorationId: restorationId, restorationId: restorationId,
scrollBehavior: scrollBehavior,
); );
final double itemExtent; final double itemExtent;
...@@ -584,6 +587,7 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -584,6 +587,7 @@ class ListWheelScrollView extends StatefulWidget {
this.renderChildrenOutsideViewport = false, this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId, this.restorationId,
this.scrollBehavior,
required List<Widget> children, required List<Widget> children,
}) : assert(children != null), }) : assert(children != null),
assert(diameterRatio != null), assert(diameterRatio != null),
...@@ -625,6 +629,7 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -625,6 +629,7 @@ class ListWheelScrollView extends StatefulWidget {
this.renderChildrenOutsideViewport = false, this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId, this.restorationId,
this.scrollBehavior,
required this.childDelegate, required this.childDelegate,
}) : assert(childDelegate != null), }) : assert(childDelegate != null),
assert(diameterRatio != null), assert(diameterRatio != null),
...@@ -669,6 +674,10 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -669,6 +674,10 @@ class ListWheelScrollView extends StatefulWidget {
/// For example, determines how the scroll view continues to animate after the /// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view. /// user stops dragging the scroll view.
/// ///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions. /// Defaults to matching platform conventions.
final ScrollPhysics? physics; final ScrollPhysics? physics;
...@@ -716,6 +725,17 @@ class ListWheelScrollView extends StatefulWidget { ...@@ -716,6 +725,17 @@ class ListWheelScrollView extends StatefulWidget {
/// {@macro flutter.widgets.scrollable.restorationId} /// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId; final String? restorationId;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
@override @override
_ListWheelScrollViewState createState() => _ListWheelScrollViewState(); _ListWheelScrollViewState createState() => _ListWheelScrollViewState();
} }
...@@ -769,6 +789,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> { ...@@ -769,6 +789,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
physics: widget.physics, physics: widget.physics,
itemExtent: widget.itemExtent, itemExtent: widget.itemExtent,
restorationId: widget.restorationId, restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return ListWheelViewport( return ListWheelViewport(
diameterRatio: widget.diameterRatio, diameterRatio: widget.diameterRatio,
......
...@@ -13,6 +13,7 @@ import 'basic.dart'; ...@@ -13,6 +13,7 @@ import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'primary_scroll_controller.dart'; import 'primary_scroll_controller.dart';
import 'scroll_activity.dart'; import 'scroll_activity.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart'; import 'scroll_context.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_metrics.dart'; import 'scroll_metrics.dart';
...@@ -369,6 +370,7 @@ class NestedScrollView extends StatefulWidget { ...@@ -369,6 +370,7 @@ class NestedScrollView extends StatefulWidget {
this.floatHeaderSlivers = false, this.floatHeaderSlivers = false,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId, this.restorationId,
this.scrollBehavior,
}) : assert(scrollDirection != null), }) : assert(scrollDirection != null),
assert(reverse != null), assert(reverse != null),
assert(headerSliverBuilder != null), assert(headerSliverBuilder != null),
...@@ -407,6 +409,10 @@ class NestedScrollView extends StatefulWidget { ...@@ -407,6 +409,10 @@ class NestedScrollView extends StatefulWidget {
/// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of /// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of
/// the physics to be overridden). /// the physics to be overridden).
/// ///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions. /// Defaults to matching platform conventions.
/// ///
/// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided /// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided
...@@ -453,6 +459,20 @@ class NestedScrollView extends StatefulWidget { ...@@ -453,6 +459,20 @@ class NestedScrollView extends StatefulWidget {
/// {@macro flutter.widgets.scrollable.restorationId} /// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId; final String? restorationId;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar]. This is because the
/// NestedScrollView cannot assume the configuration of the outer and inner
/// [Scrollable] widgets, particularly whether to treat them as one scrollable,
/// or separate and desirous of unique behaviors.
final ScrollBehavior? scrollBehavior;
/// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor /// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
/// [NestedScrollView]. /// [NestedScrollView].
/// ///
...@@ -613,6 +633,10 @@ class NestedScrollViewState extends State<NestedScrollView> { ...@@ -613,6 +633,10 @@ class NestedScrollViewState extends State<NestedScrollView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ScrollPhysics _scrollPhysics = widget.physics?.applyTo(const ClampingScrollPhysics())
?? widget.scrollBehavior?.getScrollPhysics(context).applyTo(const ClampingScrollPhysics())
?? const ClampingScrollPhysics();
return _InheritedNestedScrollView( return _InheritedNestedScrollView(
state: this, state: this,
child: Builder( child: Builder(
...@@ -622,9 +646,8 @@ class NestedScrollViewState extends State<NestedScrollView> { ...@@ -622,9 +646,8 @@ class NestedScrollViewState extends State<NestedScrollView> {
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
scrollDirection: widget.scrollDirection, scrollDirection: widget.scrollDirection,
reverse: widget.reverse, reverse: widget.reverse,
physics: widget.physics != null physics: _scrollPhysics,
? widget.physics!.applyTo(const ClampingScrollPhysics()) scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
: const ClampingScrollPhysics(),
controller: _coordinator!._outerController, controller: _coordinator!._outerController,
slivers: widget._buildSlivers( slivers: widget._buildSlivers(
context, context,
...@@ -646,6 +669,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView { ...@@ -646,6 +669,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
required Axis scrollDirection, required Axis scrollDirection,
required bool reverse, required bool reverse,
required ScrollPhysics physics, required ScrollPhysics physics,
required ScrollBehavior scrollBehavior,
required ScrollController controller, required ScrollController controller,
required List<Widget> slivers, required List<Widget> slivers,
required this.handle, required this.handle,
...@@ -656,6 +680,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView { ...@@ -656,6 +680,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
physics: physics, physics: physics,
scrollBehavior: scrollBehavior,
controller: controller, controller: controller,
slivers: slivers, slivers: slivers,
dragStartBehavior: dragStartBehavior, dragStartBehavior: dragStartBehavior,
......
...@@ -27,7 +27,7 @@ import 'ticker_provider.dart'; ...@@ -27,7 +27,7 @@ import 'ticker_provider.dart';
/// showing the indication, call [OverscrollIndicatorNotification.disallowGlow] /// showing the indication, call [OverscrollIndicatorNotification.disallowGlow]
/// on the notification. /// on the notification.
/// ///
/// Created automatically by [ScrollBehavior.buildViewportChrome] on platforms /// Created automatically by [ScrollBehavior.buildOverscrollIndicator] on platforms
/// (e.g., Android) that commonly use this type of overscroll indication. /// (e.g., Android) that commonly use this type of overscroll indication.
/// ///
/// In a [MaterialApp], the edge glow color is the overall theme's /// In a [MaterialApp], the edge glow color is the overall theme's
...@@ -189,7 +189,7 @@ class GlowingOverscrollIndicator extends StatefulWidget { ...@@ -189,7 +189,7 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// subtree) should include a source of [ScrollNotification] notifications. /// subtree) should include a source of [ScrollNotification] notifications.
/// ///
/// Typically a [GlowingOverscrollIndicator] is created by a /// Typically a [GlowingOverscrollIndicator] is created by a
/// [ScrollBehavior.buildViewportChrome] method, in which case /// [ScrollBehavior.buildOverscrollIndicator] method, in which case
/// the child is usually the one provided as an argument to that method. /// the child is usually the one provided as an argument to that method.
final Widget? child; final Widget? child;
......
...@@ -13,6 +13,7 @@ import 'debug.dart'; ...@@ -13,6 +13,7 @@ import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'page_storage.dart'; import 'page_storage.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart'; import 'scroll_context.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_metrics.dart'; import 'scroll_metrics.dart';
...@@ -635,6 +636,7 @@ class PageView extends StatefulWidget { ...@@ -635,6 +636,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false, this.allowImplicitScrolling = false,
this.restorationId, this.restorationId,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(allowImplicitScrolling != null), }) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null), assert(clipBehavior != null),
controller = controller ?? _defaultPageController, controller = controller ?? _defaultPageController,
...@@ -673,6 +675,7 @@ class PageView extends StatefulWidget { ...@@ -673,6 +675,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false, this.allowImplicitScrolling = false,
this.restorationId, this.restorationId,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(allowImplicitScrolling != null), }) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null), assert(clipBehavior != null),
controller = controller ?? _defaultPageController, controller = controller ?? _defaultPageController,
...@@ -776,6 +779,7 @@ class PageView extends StatefulWidget { ...@@ -776,6 +779,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false, this.allowImplicitScrolling = false,
this.restorationId, this.restorationId,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(childrenDelegate != null), }) : assert(childrenDelegate != null),
assert(allowImplicitScrolling != null), assert(allowImplicitScrolling != null),
assert(clipBehavior != null), assert(clipBehavior != null),
...@@ -829,6 +833,10 @@ class PageView extends StatefulWidget { ...@@ -829,6 +833,10 @@ class PageView extends StatefulWidget {
/// The physics are modified to snap to page boundaries using /// The physics are modified to snap to page boundaries using
/// [PageScrollPhysics] prior to being used. /// [PageScrollPhysics] prior to being used.
/// ///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// Defaults to matching platform conventions. /// Defaults to matching platform conventions.
final ScrollPhysics? physics; final ScrollPhysics? physics;
...@@ -854,6 +862,17 @@ class PageView extends StatefulWidget { ...@@ -854,6 +862,17 @@ class PageView extends StatefulWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
final Clip clipBehavior; final Clip clipBehavior;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
///
/// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
/// modified by default to not apply a [Scrollbar].
final ScrollBehavior? scrollBehavior;
@override @override
_PageViewState createState() => _PageViewState(); _PageViewState createState() => _PageViewState();
} }
...@@ -885,8 +904,8 @@ class _PageViewState extends State<PageView> { ...@@ -885,8 +904,8 @@ class _PageViewState extends State<PageView> {
final ScrollPhysics physics = _ForceImplicitScrollPhysics( final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling, allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(widget.pageSnapping ).applyTo(widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics) ? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics); : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context));
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) { onNotification: (ScrollNotification notification) {
...@@ -906,6 +925,7 @@ class _PageViewState extends State<PageView> { ...@@ -906,6 +925,7 @@ class _PageViewState extends State<PageView> {
controller: widget.controller, controller: widget.controller,
physics: physics, physics: physics,
restorationId: widget.restorationId, restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) { viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport( return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent // TODO(dnfield): we should provide a way to set cacheExtent
......
...@@ -9,6 +9,8 @@ import 'package:flutter/rendering.dart'; ...@@ -9,6 +9,8 @@ import 'package:flutter/rendering.dart';
import 'framework.dart'; import 'framework.dart';
import 'overscroll_indicator.dart'; import 'overscroll_indicator.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart';
import 'scrollbar.dart';
const Color _kDefaultGlowColor = Color(0xFFFFFFFF); const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
...@@ -21,8 +23,13 @@ const Color _kDefaultGlowColor = Color(0xFFFFFFFF); ...@@ -21,8 +23,13 @@ const Color _kDefaultGlowColor = Color(0xFFFFFFFF);
/// This class can be extended to further customize a [ScrollBehavior] for a /// This class can be extended to further customize a [ScrollBehavior] for a
/// subtree. For example, overriding [ScrollBehavior.getScrollPhysics] sets the /// subtree. For example, overriding [ScrollBehavior.getScrollPhysics] sets the
/// default [ScrollPhysics] for [Scrollable]s that inherit this [ScrollConfiguration]. /// default [ScrollPhysics] for [Scrollable]s that inherit this [ScrollConfiguration].
/// Overriding [ScrollBehavior.buildViewportChrome] can be used to add or change /// Overriding [ScrollBehavior.buildOverscrollIndicator] can be used to add or change
/// default decorations like [GlowingOverscrollIndicator]s. /// 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.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// See also: /// See also:
...@@ -34,6 +41,29 @@ class ScrollBehavior { ...@@ -34,6 +41,29 @@ class ScrollBehavior {
/// Creates a description of how [Scrollable] widgets should behave. /// Creates a description of how [Scrollable] widgets should behave.
const ScrollBehavior(); const ScrollBehavior();
/// 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({
bool scrollbars = true,
bool overscroll = true,
ScrollPhysics? physics,
TargetPlatform? platform,
}) {
return _WrappedScrollBehavior(
delegate: this,
scrollbar: scrollbars,
overscrollIndicator: overscroll,
physics: physics,
platform: platform,
);
}
/// The platform whose scroll physics should be implemented. /// The platform whose scroll physics should be implemented.
/// ///
/// Defaults to the current platform. /// Defaults to the current platform.
...@@ -44,9 +74,14 @@ class ScrollBehavior { ...@@ -44,9 +74,14 @@ class ScrollBehavior {
/// For example, on Android, this method wraps the given widget with a /// For example, on Android, this method wraps the given widget with a
/// [GlowingOverscrollIndicator] to provide visual feedback when the user /// [GlowingOverscrollIndicator] to provide visual feedback when the user
/// overscrolls. /// overscrolls.
///
/// This method is deprecated. Use [ScrollBehavior.buildOverscrollIndicator]
/// instead.
@Deprecated(
'Migrate to buildOverscrollIndicator. '
'This feature was deprecated after v2.1.0-11.0.pre.'
)
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
// When modifying this function, consider modifying the implementation in
// MaterialScrollBehavior as well.
switch (getPlatform(context)) { switch (getPlatform(context)) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.linux: case TargetPlatform.linux:
...@@ -55,14 +90,43 @@ class ScrollBehavior { ...@@ -55,14 +90,43 @@ class ScrollBehavior {
return child; return child;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: _kDefaultGlowColor,
);
}
}
/// 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(
child: child, child: child,
axisDirection: axisDirection, controller: details.controller,
color: _kDefaultGlowColor,
); );
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
return child;
} }
} }
/// Applies a [GlowingOverscrollIndicator] to the child widget on
/// [TargetPlatform.android] and [TargetPlatform.fuchsia].
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
// TODO(Piinks): Move implementation from buildViewportChrome here after
// deprecation period
// When modifying this function, consider modifying the implementation in
// the Material and Cupertino subclasses as well.
return buildViewportChrome(context, child, details.direction);
}
/// Specifies the type of velocity tracker to use in the descendant /// Specifies the type of velocity tracker to use in the descendant
/// [Scrollable]s' drag gesture recognizers, for estimating the velocity of a /// [Scrollable]s' drag gesture recognizers, for estimating the velocity of a
/// drag gesture. /// drag gesture.
...@@ -129,6 +193,84 @@ class ScrollBehavior { ...@@ -129,6 +193,84 @@ class ScrollBehavior {
String toString() => objectRuntimeType(this, 'ScrollBehavior'); String toString() => objectRuntimeType(this, 'ScrollBehavior');
} }
class _WrappedScrollBehavior implements ScrollBehavior {
const _WrappedScrollBehavior({
required this.delegate,
this.scrollbar = true,
this.overscrollIndicator = true,
this.physics,
this.platform,
});
final ScrollBehavior delegate;
final bool scrollbar;
final bool overscrollIndicator;
final ScrollPhysics? physics;
final TargetPlatform? platform;
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
if (overscrollIndicator)
return delegate.buildOverscrollIndicator(context, child, details);
return child;
}
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
if (scrollbar)
return delegate.buildScrollbar(context, child, details);
return child;
}
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
return delegate.buildViewportChrome(context, child, axisDirection);
}
@override
ScrollBehavior copyWith({
bool scrollbars = true,
bool overscroll = true,
ScrollPhysics? physics,
TargetPlatform? platform,
}) {
return delegate.copyWith(
scrollbars: scrollbars,
overscroll: overscroll,
physics: physics,
platform: platform,
);
}
@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
|| oldDelegate.scrollbar != scrollbar
|| oldDelegate.overscrollIndicator != overscrollIndicator
|| oldDelegate.physics != physics
|| oldDelegate.platform != platform
|| delegate.shouldNotify(oldDelegate.delegate);
}
@override
GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
return delegate.velocityTrackerBuilder(context);
}
@override
String toString() => objectRuntimeType(this, '_WrappedScrollBehavior');
}
/// Controls how [Scrollable] widgets behave in a subtree. /// Controls how [Scrollable] widgets behave in a subtree.
/// ///
/// The scroll configuration determines the [ScrollPhysics] and viewport /// The scroll configuration determines the [ScrollPhysics] and viewport
......
...@@ -24,7 +24,7 @@ abstract class ScrollContext { ...@@ -24,7 +24,7 @@ abstract class ScrollContext {
/// This context is typically different that the context of the scrollable /// This context is typically different that the context of the scrollable
/// widget itself. For example, [Scrollable] uses a context outside the /// widget itself. For example, [Scrollable] uses a context outside the
/// [Viewport] but inside the widgets created by /// [Viewport] but inside the widgets created by
/// [ScrollBehavior.buildViewportChrome]. /// [ScrollBehavior.buildOverscrollIndicator] and [ScrollBehavior.buildScrollbar].
BuildContext? get notificationContext; BuildContext? get notificationContext;
/// The [BuildContext] that should be used when searching for a [PageStorage]. /// The [BuildContext] that should be used when searching for a [PageStorage].
......
...@@ -15,6 +15,7 @@ import 'framework.dart'; ...@@ -15,6 +15,7 @@ import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'primary_scroll_controller.dart'; import 'primary_scroll_controller.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
...@@ -83,6 +84,7 @@ abstract class ScrollView extends StatelessWidget { ...@@ -83,6 +84,7 @@ abstract class ScrollView extends StatelessWidget {
this.controller, this.controller,
bool? primary, bool? primary,
ScrollPhysics? physics, ScrollPhysics? physics,
this.scrollBehavior,
this.shrinkWrap = false, this.shrinkWrap = false,
this.center, this.center,
this.anchor = 0.0, this.anchor = 0.0,
...@@ -205,8 +207,20 @@ abstract class ScrollView extends StatelessWidget { ...@@ -205,8 +207,20 @@ abstract class ScrollView extends StatelessWidget {
/// inefficient to speculatively create this object each frame to see if the /// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.) /// physics should be updated.)
/// {@endtemplate} /// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics; final ScrollPhysics? physics;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
final ScrollBehavior? scrollBehavior;
/// {@template flutter.widgets.scroll_view.shrinkWrap} /// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be /// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed. /// determined by the contents being viewed.
...@@ -380,6 +394,7 @@ abstract class ScrollView extends StatelessWidget { ...@@ -380,6 +394,7 @@ abstract class ScrollView extends StatelessWidget {
axisDirection: axisDirection, axisDirection: axisDirection,
controller: scrollController, controller: scrollController,
physics: physics, physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount, semanticChildCount: semanticChildCount,
restorationId: restorationId, restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
...@@ -610,6 +625,7 @@ class CustomScrollView extends ScrollView { ...@@ -610,6 +625,7 @@ class CustomScrollView extends ScrollView {
ScrollController? controller, ScrollController? controller,
bool? primary, bool? primary,
ScrollPhysics? physics, ScrollPhysics? physics,
ScrollBehavior? scrollBehavior,
bool shrinkWrap = false, bool shrinkWrap = false,
Key? center, Key? center,
double anchor = 0.0, double anchor = 0.0,
...@@ -627,6 +643,7 @@ class CustomScrollView extends ScrollView { ...@@ -627,6 +643,7 @@ class CustomScrollView extends ScrollView {
controller: controller, controller: controller,
primary: primary, primary: primary,
physics: physics, physics: physics,
scrollBehavior: scrollBehavior,
shrinkWrap: shrinkWrap, shrinkWrap: shrinkWrap,
center: center, center: center,
anchor: anchor, anchor: anchor,
......
...@@ -25,7 +25,6 @@ import 'scroll_controller.dart'; ...@@ -25,7 +25,6 @@ import 'scroll_controller.dart';
import 'scroll_metrics.dart'; import 'scroll_metrics.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'viewport.dart'; import 'viewport.dart';
...@@ -91,6 +90,7 @@ class Scrollable extends StatefulWidget { ...@@ -91,6 +90,7 @@ class Scrollable extends StatefulWidget {
this.semanticChildCount, this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.restorationId, this.restorationId,
this.scrollBehavior,
}) : assert(axisDirection != null), }) : assert(axisDirection != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
assert(viewportBuilder != null), assert(viewportBuilder != null),
...@@ -135,6 +135,10 @@ class Scrollable extends StatefulWidget { ...@@ -135,6 +135,10 @@ class Scrollable extends StatefulWidget {
/// Defaults to matching platform conventions via the physics provided from /// Defaults to matching platform conventions via the physics provided from
/// the ambient [ScrollConfiguration]. /// the ambient [ScrollConfiguration].
/// ///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
///
/// The physics can be changed dynamically, but new physics will only take /// The physics can be changed dynamically, but new physics will only take
/// effect if the _class_ of the provided object changes. Merely constructing /// effect if the _class_ of the provided object changes. Merely constructing
/// a new instance with a different configuration is insufficient to cause the /// a new instance with a different configuration is insufficient to cause the
...@@ -243,6 +247,14 @@ class Scrollable extends StatefulWidget { ...@@ -243,6 +247,14 @@ class Scrollable extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final String? restorationId; final String? restorationId;
/// {@macro flutter.widgets.shadow.scrollBehavior}
///
/// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
/// [ScrollPhysics] is provided in [physics], it will take precedence,
/// followed by [scrollBehavior], and then the inherited ancestor
/// [ScrollBehavior].
final ScrollBehavior? scrollBehavior;
/// The axis along which the scroll view scrolls. /// The axis along which the scroll view scrolls.
/// ///
/// Determined by the [axisDirection]. /// Determined by the [axisDirection].
...@@ -385,27 +397,31 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -385,27 +397,31 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
late ScrollBehavior _configuration; late ScrollBehavior _configuration;
ScrollPhysics? _physics; ScrollPhysics? _physics;
ScrollController? _fallbackScrollController;
ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
// Only call this from places that will definitely trigger a rebuild. // Only call this from places that will definitely trigger a rebuild.
void _updatePosition() { void _updatePosition() {
_configuration = ScrollConfiguration.of(context); _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context); _physics = _configuration.getScrollPhysics(context);
if (widget.physics != null) if (widget.physics != null) {
_physics = widget.physics!.applyTo(_physics); _physics = widget.physics!.applyTo(_physics);
final ScrollController? controller = widget.controller; } else if (widget.scrollBehavior != null) {
_physics = widget.scrollBehavior!.getScrollPhysics(context).applyTo(_physics);
}
final ScrollPosition? oldPosition = _position; final ScrollPosition? oldPosition = _position;
if (oldPosition != null) { if (oldPosition != null) {
controller?.detach(oldPosition); _effectiveScrollController.detach(oldPosition);
// It's important that we not dispose the old position until after the // It's important that we not dispose the old position until after the
// viewport has had a chance to unregister its listeners from the old // viewport has had a chance to unregister its listeners from the old
// position. So, schedule a microtask to do it. // position. So, schedule a microtask to do it.
scheduleMicrotask(oldPosition.dispose); scheduleMicrotask(oldPosition.dispose);
} }
_position = controller?.createScrollPosition(_physics!, this, oldPosition) _position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPosition);
?? ScrollPositionWithSingleContext(physics: _physics!, context: this, oldPosition: oldPosition);
assert(_position != null); assert(_position != null);
controller?.attach(position); _effectiveScrollController.attach(position);
} }
@override @override
...@@ -426,6 +442,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -426,6 +442,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
ServicesBinding.instance!.restorationManager.flushData(); ServicesBinding.instance!.restorationManager.flushData();
} }
@override
void initState() {
if (widget.controller == null)
_fallbackScrollController = ScrollController();
super.initState();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
_updatePosition(); _updatePosition();
...@@ -433,8 +456,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -433,8 +456,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
} }
bool _shouldUpdatePosition(Scrollable oldWidget) { bool _shouldUpdatePosition(Scrollable oldWidget) {
ScrollPhysics? newPhysics = widget.physics; ScrollPhysics? newPhysics = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context);
ScrollPhysics? oldPhysics = oldWidget.physics; ScrollPhysics? oldPhysics = oldWidget.physics ?? oldWidget.scrollBehavior?.getScrollPhysics(context);
do { do {
if (newPhysics?.runtimeType != oldPhysics?.runtimeType) if (newPhysics?.runtimeType != oldPhysics?.runtimeType)
return true; return true;
...@@ -450,8 +473,25 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -450,8 +473,25 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) { if (widget.controller != oldWidget.controller) {
oldWidget.controller?.detach(position); if (oldWidget.controller == null) {
widget.controller?.attach(position); // The old controller was null, meaning the fallback cannot be null.
// Dispose of the fallback.
assert(_fallbackScrollController != null);
assert(widget.controller != null);
_fallbackScrollController!.detach(position);
_fallbackScrollController!.dispose();
_fallbackScrollController = null;
} else {
// The old controller was not null, detach.
oldWidget.controller?.detach(position);
if (widget.controller == null) {
// If the new controller is null, we need to set up the fallback
// ScrollController.
_fallbackScrollController = ScrollController();
}
}
// Attach the updated effective scroll controller.
_effectiveScrollController.attach(position);
} }
if (_shouldUpdatePosition(oldWidget)) if (_shouldUpdatePosition(oldWidget))
...@@ -460,7 +500,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -460,7 +500,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
@override @override
void dispose() { void dispose() {
widget.controller?.detach(position); if (widget.controller != null) {
widget.controller!.detach(position);
} else {
_fallbackScrollController?.detach(position);
_fallbackScrollController?.dispose();
}
position.dispose(); position.dispose();
_persistedScrollOffset.dispose(); _persistedScrollOffset.dispose();
super.dispose(); super.dispose();
...@@ -717,7 +763,16 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -717,7 +763,16 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
); );
} }
return _configuration.buildViewportChrome(context, result, widget.axisDirection); final ScrollableDetails details = ScrollableDetails(
direction: widget.axisDirection,
controller: _effectiveScrollController,
);
return _configuration.buildScrollbar(
context,
_configuration.buildOverscrollIndicator(context, result, details),
details,
);
} }
@override @override
...@@ -731,6 +786,33 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -731,6 +786,33 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
String? get restorationId => widget.restorationId; String? get restorationId => widget.restorationId;
} }
/// Describes the aspects of a Scrollable widget to inform inherited widgets
/// like [ScrollBehavior] for decorating.
///
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
/// information about the Scrollable in order to be initialized.
@immutable
class ScrollableDetails {
/// Creates a set of details describing the [Scrollable]. The [direction]
/// cannot be null.
const ScrollableDetails({
required this.direction,
required this.controller,
});
/// The direction in which this widget scrolls.
///
/// Cannot be null.
final AxisDirection direction;
/// A [ScrollController] that can be used to control the position of the
/// [Scrollable] widget.
///
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
/// [Scrollable].
final ScrollController controller;
}
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be /// With [_ScrollSemantics] certain child [SemanticsNode]s can be
/// excluded from the scrollable area for semantics purposes. /// excluded from the scrollable area for semantics purposes.
/// ///
......
...@@ -584,7 +584,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -584,7 +584,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// ///
/// If the scrollbar is wrapped around multiple [ScrollView]s, it only responds to /// If the scrollbar is wrapped around multiple [ScrollView]s, it only responds to
/// the nearest scrollView and shows the corresponding scrollbar thumb by default. /// the nearest scrollView and shows the corresponding scrollbar thumb by default.
/// Set [notificationPredicate] to something else for more complicated behaviors. /// The [notificationPredicate] allows the ability to customize which
/// [ScrollNotification]s the Scrollbar should listen to.
/// ///
/// Scrollbars are interactive and will also use the [PrimaryScrollController] if /// Scrollbars are interactive and will also use the [PrimaryScrollController] if
/// a [controller] is not set. Scrollbar thumbs can be dragged along the main axis /// a [controller] is not set. Scrollbar thumbs can be dragged along the main axis
...@@ -596,6 +597,17 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -596,6 +597,17 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// painted. In this case, the scrollbar cannot accurately represent the /// painted. In this case, the scrollbar cannot accurately represent the
/// relative location of the visible area, or calculate the accurate delta to /// relative location of the visible area, or calculate the accurate delta to
/// apply when dragging on the thumb or tapping on the track. /// apply when dragging on the thumb or tapping on the track.
///
/// Scrollbars are added to most [Scrollable] widgets by default on Desktop
/// platforms in [ScrollBehavior.buildScrollbar] as part of an app's
/// [ScrollConfiguration]. Scrollable widgets that do not have automatically
/// applied Scrollbars include
///
/// * [EditableText]
/// * [ListWheelScrollView]
/// * [PageView]
/// * [NestedScrollView]
/// * [DropdownButton]
/// {@endtemplate} /// {@endtemplate}
/// ///
// TODO(Piinks): Add code sample // TODO(Piinks): Add code sample
...@@ -615,8 +627,8 @@ class RawScrollbar extends StatefulWidget { ...@@ -615,8 +627,8 @@ class RawScrollbar extends StatefulWidget {
/// The [child], or a descendant of the [child], should be a source of /// The [child], or a descendant of the [child], should be a source of
/// [ScrollNotification] notifications, typically a [Scrollable] widget. /// [ScrollNotification] notifications, typically a [Scrollable] widget.
/// ///
/// The [child], [thickness], [thumbColor], [isAlwaysShown], [fadeDuration], /// The [child], [fadeDuration], [pressDuration], and [timeToFade] arguments
/// and [timeToFade] arguments must not be null. /// must not be null.
const RawScrollbar({ const RawScrollbar({
Key? key, Key? key,
required this.child, required this.child,
...@@ -641,6 +653,9 @@ class RawScrollbar extends StatefulWidget { ...@@ -641,6 +653,9 @@ class RawScrollbar extends StatefulWidget {
/// ///
/// The scrollbar will be stacked on top of this child. This child (and its /// The scrollbar will be stacked on top of this child. This child (and its
/// subtree) should include a source of [ScrollNotification] notifications. /// subtree) should include a source of [ScrollNotification] notifications.
/// Typically a [Scrollbar] is created on desktop platforms by a
/// [ScrollBehavior.buildScrollbar] method, in which case the child is usually
/// the one provided as an argument to that method.
/// ///
/// Typically a [ListView] or [CustomScrollView]. /// Typically a [ListView] or [CustomScrollView].
/// {@endtemplate} /// {@endtemplate}
......
...@@ -198,7 +198,7 @@ void main() { ...@@ -198,7 +198,7 @@ void main() {
late BuildContext capturedContext; late BuildContext capturedContext;
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
scrollBehavior: MockScrollBehavior(), scrollBehavior: const MockScrollBehavior(),
home: Builder( home: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
capturedContext = context; capturedContext = context;
...@@ -214,6 +214,8 @@ void main() { ...@@ -214,6 +214,8 @@ void main() {
} }
class MockScrollBehavior extends ScrollBehavior { class MockScrollBehavior extends ScrollBehavior {
const MockScrollBehavior();
@override @override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
} }
......
...@@ -956,7 +956,6 @@ void main() { ...@@ -956,7 +956,6 @@ void main() {
}, },
); );
testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async { testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
WidgetsApp( WidgetsApp(
......
...@@ -34,9 +34,8 @@ void main() { ...@@ -34,9 +34,8 @@ void main() {
void uiTestGroup() { void uiTestGroup() {
testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async { testWidgets("doesn't invoke anything without user interaction", (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -57,9 +56,8 @@ void main() { ...@@ -57,9 +56,8 @@ void main() {
testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async { testWidgets('calls the indicator builder when starting to overscroll', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -96,13 +94,16 @@ void main() { ...@@ -96,13 +94,16 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: CustomScrollView( child: MediaQuery(
slivers: <Widget>[ data: const MediaQueryData(),
CupertinoSliverRefreshControl( child: CustomScrollView(
builder: mockHelper.builder, slivers: <Widget>[
), CupertinoSliverRefreshControl(
buildAListOfStuff(), builder: mockHelper.builder,
], ),
buildAListOfStuff(),
],
),
), ),
), ),
); );
...@@ -121,9 +122,8 @@ void main() { ...@@ -121,9 +122,8 @@ void main() {
testWidgets('let the builder update as canceled drag scrolls away', (WidgetTester tester) async { testWidgets('let the builder update as canceled drag scrolls away', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -178,9 +178,8 @@ void main() { ...@@ -178,9 +178,8 @@ void main() {
}); });
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -234,9 +233,8 @@ void main() { ...@@ -234,9 +233,8 @@ void main() {
'refreshing task keeps the sliver expanded forever until done', 'refreshing task keeps the sliver expanded forever until done',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -308,9 +306,8 @@ void main() { ...@@ -308,9 +306,8 @@ void main() {
() async { () async {
mockHelper.refreshCompleter = Completer<void>.sync(); mockHelper.refreshCompleter = Completer<void>.sync();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -382,9 +379,8 @@ void main() { ...@@ -382,9 +379,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -456,9 +452,8 @@ void main() { ...@@ -456,9 +452,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -525,9 +520,8 @@ void main() { ...@@ -525,9 +520,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -608,9 +602,8 @@ void main() { ...@@ -608,9 +602,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -689,9 +682,8 @@ void main() { ...@@ -689,9 +682,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -745,9 +737,8 @@ void main() { ...@@ -745,9 +737,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -825,9 +816,8 @@ void main() { ...@@ -825,9 +816,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
buildAListOfStuff(), buildAListOfStuff(),
CupertinoSliverRefreshControl( // it's in the middle now. CupertinoSliverRefreshControl( // it's in the middle now.
...@@ -852,9 +842,8 @@ void main() { ...@@ -852,9 +842,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -894,9 +883,8 @@ void main() { ...@@ -894,9 +883,8 @@ void main() {
testWidgets('Should not crash when dragged', (WidgetTester tester) async { testWidgets('Should not crash when dragged', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
...@@ -961,9 +949,8 @@ void main() { ...@@ -961,9 +949,8 @@ void main() {
void stateMachineTestGroup() { void stateMachineTestGroup() {
testWidgets('starts in inactive state', (WidgetTester tester) async { testWidgets('starts in inactive state', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -982,9 +969,8 @@ void main() { ...@@ -982,9 +969,8 @@ void main() {
testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async { testWidgets('goes to drag and returns to inactive in a small drag', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1013,9 +999,8 @@ void main() { ...@@ -1013,9 +999,8 @@ void main() {
testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async { testWidgets('goes to armed the frame it passes the threshold', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1047,9 +1032,8 @@ void main() { ...@@ -1047,9 +1032,8 @@ void main() {
'goes to refresh the frame it crossed back the refresh threshold', 'goes to refresh the frame it crossed back the refresh threshold',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1087,9 +1071,8 @@ void main() { ...@@ -1087,9 +1071,8 @@ void main() {
'goes to done internally as soon as the task finishes', 'goes to done internally as soon as the task finishes',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1134,9 +1117,8 @@ void main() { ...@@ -1134,9 +1117,8 @@ void main() {
'goes back to inactive when retracting back past 10% of arming distance', 'goes back to inactive when retracting back past 10% of arming distance',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1192,9 +1174,8 @@ void main() { ...@@ -1192,9 +1174,8 @@ void main() {
'goes back to inactive if already scrolled away when task completes', 'goes back to inactive if already scrolled away when task completes',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: mockHelper.builder, builder: mockHelper.builder,
...@@ -1253,9 +1234,8 @@ void main() { ...@@ -1253,9 +1234,8 @@ void main() {
mockHelper.refreshIndicator = const Center(child: Text('-1')); mockHelper.refreshIndicator = const Center(child: Text('-1'));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
CupertinoSliverRefreshControl( CupertinoSliverRefreshControl(
builder: null, builder: null,
...@@ -1298,9 +1278,8 @@ void main() { ...@@ -1298,9 +1278,8 @@ void main() {
testWidgets('buildRefreshIndicator progress', (WidgetTester tester) async { testWidgets('buildRefreshIndicator progress', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: Builder(
child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return CupertinoSliverRefreshControl.buildRefreshIndicator( return CupertinoSliverRefreshControl.buildRefreshIndicator(
context, context,
...@@ -1314,9 +1293,8 @@ void main() { ...@@ -1314,9 +1293,8 @@ void main() {
expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 10.0 / 100.0); expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 10.0 / 100.0);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: Builder(
child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return CupertinoSliverRefreshControl.buildRefreshIndicator( return CupertinoSliverRefreshControl.buildRefreshIndicator(
context, context,
...@@ -1330,9 +1308,8 @@ void main() { ...@@ -1330,9 +1308,8 @@ void main() {
expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 26.0 / 100.0); expect(tester.widget<CupertinoActivityIndicator>(find.byType(CupertinoActivityIndicator)).progress, 26.0 / 100.0);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: Builder(
child: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return CupertinoSliverRefreshControl.buildRefreshIndicator( return CupertinoSliverRefreshControl.buildRefreshIndicator(
context, context,
...@@ -1367,9 +1344,8 @@ void main() { ...@@ -1367,9 +1344,8 @@ void main() {
(WidgetTester tester) async { (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/46871. // Regression test for https://github.com/flutter/flutter/issues/46871.
await tester.pumpWidget( await tester.pumpWidget(
Directionality( CupertinoApp(
textDirection: TextDirection.ltr, home: CustomScrollView(
child: CustomScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
const CupertinoSliverRefreshControl(), const CupertinoSliverRefreshControl(),
......
...@@ -1046,7 +1046,7 @@ void main() { ...@@ -1046,7 +1046,7 @@ void main() {
late BuildContext capturedContext; late BuildContext capturedContext;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
scrollBehavior: MockScrollBehavior(), scrollBehavior: const MockScrollBehavior(),
home: Builder( home: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
capturedContext = context; capturedContext = context;
...@@ -1062,6 +1062,8 @@ void main() { ...@@ -1062,6 +1062,8 @@ void main() {
} }
class MockScrollBehavior extends ScrollBehavior { class MockScrollBehavior extends ScrollBehavior {
const MockScrollBehavior();
@override @override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
} }
......
...@@ -49,11 +49,21 @@ Widget _buildBoilerplate({ ...@@ -49,11 +49,21 @@ Widget _buildBoilerplate({
textDirection: textDirection, textDirection: textDirection,
child: MediaQuery( child: MediaQuery(
data: MediaQueryData(padding: padding), data: MediaQueryData(padding: padding),
child: child, child: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: child,
),
), ),
); );
} }
class NoScrollbarBehavior extends MaterialScrollBehavior {
const NoScrollbarBehavior();
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
}
void main() { void main() {
testWidgets("Scrollbar doesn't show when tapping list", (WidgetTester tester) async { testWidgets("Scrollbar doesn't show when tapping list", (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -809,18 +819,11 @@ void main() { ...@@ -809,18 +819,11 @@ void main() {
}); });
testWidgets('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async { testWidgets('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: PrimaryScrollController( theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(isAlwaysShown: true)),
controller: scrollController, home: const SingleChildScrollView(
child: Scrollbar( child: SizedBox(width: 4000.0, height: 4000.0)
isAlwaysShown: true,
controller: scrollController,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
), ),
), ),
); );
...@@ -858,24 +861,18 @@ void main() { ...@@ -858,24 +861,18 @@ void main() {
TargetPlatform.linux, TargetPlatform.linux,
TargetPlatform.macOS, TargetPlatform.macOS,
TargetPlatform.windows, TargetPlatform.windows,
TargetPlatform.fuchsia,
}), }),
); );
testWidgets('Hover animation is not triggered by tap gestures', (WidgetTester tester) async { testWidgets('Hover animation is not triggered by tap gestures', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: PrimaryScrollController( theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(
controller: scrollController, isAlwaysShown: true,
child: Scrollbar( showTrackOnHover: true,
isAlwaysShown: true, )),
showTrackOnHover: true, home: const SingleChildScrollView(
controller: scrollController, child: SizedBox(width: 4000.0, height: 4000.0)
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
), ),
), ),
); );
...@@ -936,27 +933,19 @@ void main() { ...@@ -936,27 +933,19 @@ void main() {
color: const Color(0x80000000), color: const Color(0x80000000),
), ),
); );
}, },
variant: const TargetPlatformVariant(<TargetPlatform>{ variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux }),
TargetPlatform.linux,
}),
); );
testWidgets('Scrollbar showTrackOnHover', (WidgetTester tester) async { testWidgets('Scrollbar showTrackOnHover', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: PrimaryScrollController( theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(
controller: scrollController, isAlwaysShown: true,
child: Scrollbar( showTrackOnHover: true,
isAlwaysShown: true, )),
showTrackOnHover: true, home: const SingleChildScrollView(
controller: scrollController, child: SizedBox(width: 4000.0, height: 4000.0)
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
), ),
), ),
); );
...@@ -1006,7 +995,6 @@ void main() { ...@@ -1006,7 +995,6 @@ void main() {
TargetPlatform.linux, TargetPlatform.linux,
TargetPlatform.macOS, TargetPlatform.macOS,
TargetPlatform.windows, TargetPlatform.windows,
TargetPlatform.fuchsia,
}), }),
); );
...@@ -1088,33 +1076,36 @@ void main() { ...@@ -1088,33 +1076,36 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: Scrollbar( child: ScrollConfiguration(
key: key2, behavior: const NoScrollbarBehavior(),
notificationPredicate: null, child: Scrollbar(
child: SingleChildScrollView( key: key2,
key: outerKey, notificationPredicate: null,
child: SizedBox( child: SingleChildScrollView(
height: 1000.0, key: outerKey,
width: double.infinity, child: SizedBox(
child: Column( height: 1000.0,
children: <Widget>[ width: double.infinity,
Scrollbar( child: Column(
key: key1, children: <Widget>[
notificationPredicate: null, Scrollbar(
child: SizedBox( key: key1,
height: 300.0, notificationPredicate: null,
width: double.infinity, child: SizedBox(
child: SingleChildScrollView( height: 300.0,
key: innerKey, width: double.infinity,
child: const SizedBox( child: SingleChildScrollView(
key: Key('Inner scrollable'), key: innerKey,
height: 1000.0, child: const SizedBox(
width: double.infinity, key: Key('Inner scrollable'),
height: 1000.0,
width: double.infinity,
),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
), ),
...@@ -1206,11 +1197,7 @@ void main() { ...@@ -1206,11 +1197,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The offset should not have changed. // The offset should not have changed.
expect(scrollController.offset, scrollAmount); expect(scrollController.offset, scrollAmount);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
TargetPlatform.linux,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
testWidgets('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async { testWidgets('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
......
...@@ -29,13 +29,16 @@ void main() { ...@@ -29,13 +29,16 @@ void main() {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scrollbar( home: ScrollConfiguration(
isAlwaysShown: true, behavior: const NoScrollbarBehavior(),
showTrackOnHover: true, child: Scrollbar(
controller: scrollController, isAlwaysShown: true,
child: SingleChildScrollView( showTrackOnHover: true,
controller: scrollController, controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0) child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
), ),
), ),
), ),
...@@ -117,13 +120,18 @@ void main() { ...@@ -117,13 +120,18 @@ void main() {
final ScrollbarThemeData scrollbarTheme = _scrollbarTheme(); final ScrollbarThemeData scrollbarTheme = _scrollbarTheme();
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: ThemeData(scrollbarTheme: scrollbarTheme), theme: ThemeData(
home: Scrollbar( scrollbarTheme: scrollbarTheme,
isAlwaysShown: true, ),
controller: scrollController, home: ScrollConfiguration(
child: SingleChildScrollView( behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
controller: scrollController, controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0) child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
), ),
), ),
)); ));
...@@ -245,12 +253,7 @@ void main() { ...@@ -245,12 +253,7 @@ void main() {
color: _kDefaultIdleThumbColor, color: _kDefaultIdleThumbColor,
), ),
); );
}, variant: const TargetPlatformVariant(<TargetPlatform>{ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
testWidgets('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async { testWidgets('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
...@@ -298,12 +301,7 @@ void main() { ...@@ -298,12 +301,7 @@ void main() {
color: _kDefaultIdleThumbColor, color: _kDefaultIdleThumbColor,
), ),
); );
}, variant: const TargetPlatformVariant(<TargetPlatform>{ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async { testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async {
const double thickness = 4.0; const double thickness = 4.0;
...@@ -314,17 +312,22 @@ void main() { ...@@ -314,17 +312,22 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()), theme: ThemeData(
home: Scrollbar( colorScheme: const ColorScheme.light(),
thickness: thickness, ),
hoverThickness: hoverThickness, home: ScrollConfiguration(
isAlwaysShown: true, behavior: const NoScrollbarBehavior(),
showTrackOnHover: showTrackOnHover, child: Scrollbar(
radius: radius, thickness: thickness,
controller: scrollController, hoverThickness: hoverThickness,
child: SingleChildScrollView( isAlwaysShown: true,
showTrackOnHover: showTrackOnHover,
radius: radius,
controller: scrollController, controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0) child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
), ),
), ),
), ),
...@@ -408,13 +411,16 @@ void main() { ...@@ -408,13 +411,16 @@ void main() {
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
return MaterialApp( return MaterialApp(
theme: appTheme, theme: appTheme,
home: Scrollbar( home: ScrollConfiguration(
isAlwaysShown: true, behavior: const NoScrollbarBehavior(),
showTrackOnHover: true, child: Scrollbar(
controller: scrollController, isAlwaysShown: true,
child: SingleChildScrollView( showTrackOnHover: true,
controller: scrollController, controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0) child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0)
),
), ),
) )
); );
...@@ -422,7 +428,9 @@ void main() { ...@@ -422,7 +428,9 @@ void main() {
// Scrollbar defaults for light themes: // Scrollbar defaults for light themes:
// - coloring based on ColorScheme.onSurface // - coloring based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.light()))); await tester.pumpWidget(buildFrame(ThemeData(
colorScheme: const ColorScheme.light(),
)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Idle scrollbar behavior // Idle scrollbar behavior
expect( expect(
...@@ -493,7 +501,9 @@ void main() { ...@@ -493,7 +501,9 @@ void main() {
// Scrollbar defaults for dark themes: // Scrollbar defaults for dark themes:
// - coloring slightly different based on ColorScheme.onSurface // - coloring slightly different based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData.from(colorScheme: const ColorScheme.dark()))); await tester.pumpWidget(buildFrame(ThemeData(
colorScheme: const ColorScheme.dark(),
)));
await tester.pumpAndSettle(); // Theme change animation await tester.pumpAndSettle(); // Theme change animation
// Idle scrollbar behavior // Idle scrollbar behavior
...@@ -617,6 +627,13 @@ void main() { ...@@ -617,6 +627,13 @@ void main() {
}, skip: kIsWeb); }, skip: kIsWeb);
} }
class NoScrollbarBehavior extends ScrollBehavior {
const NoScrollbarBehavior();
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
}
ScrollbarThemeData _scrollbarTheme({ ScrollbarThemeData _scrollbarTheme({
MaterialStateProperty<double?>? thickness, MaterialStateProperty<double?>? thickness,
bool showTrackOnHover = true, bool showTrackOnHover = true,
......
...@@ -1788,24 +1788,27 @@ void main() { ...@@ -1788,24 +1788,27 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Column( child: MediaQuery(
crossAxisAlignment: CrossAxisAlignment.start, data: const MediaQueryData(),
mainAxisAlignment: MainAxisAlignment.center, child: Column(
children: <Widget>[ crossAxisAlignment: CrossAxisAlignment.start,
GridView( mainAxisAlignment: MainAxisAlignment.center,
shrinkWrap: true, children: <Widget>[
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( GridView(
crossAxisCount: 3, shrinkWrap: true,
childAspectRatio: 3, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 3, crossAxisCount: 3,
crossAxisSpacing: 3), childAspectRatio: 3,
children: const <Widget>[ mainAxisSpacing: 3,
Text('a'), crossAxisSpacing: 3),
Text('b'), children: const <Widget>[
Text('c'), Text('a'),
], Text('b'),
), Text('c'),
], ],
),
],
),
), ),
), ),
); );
......
...@@ -327,11 +327,6 @@ void main() { ...@@ -327,11 +327,6 @@ void main() {
}); });
} }
class MockScrollBehavior extends ScrollBehavior {
@override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation); typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate); typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
......
...@@ -18,33 +18,36 @@ void main() { ...@@ -18,33 +18,36 @@ void main() {
}) { }) {
return Directionality( return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: MediaQuery(
children: <Widget>[ data: const MediaQueryData(),
TextButton( child: Stack(
child: const Text('TapHere'), children: <Widget>[
onPressed: onButtonPressed, TextButton(
), child: const Text('TapHere'),
DraggableScrollableSheet( onPressed: onButtonPressed,
maxChildSize: maxChildSize, ),
minChildSize: minChildSize, DraggableScrollableSheet(
initialChildSize: initialChildSize, maxChildSize: maxChildSize,
builder: (BuildContext context, ScrollController scrollController) { minChildSize: minChildSize,
return NotificationListener<ScrollNotification>( initialChildSize: initialChildSize,
onNotification: onScrollNotification, builder: (BuildContext context, ScrollController scrollController) {
child: Container( return NotificationListener<ScrollNotification>(
key: containerKey, onNotification: onScrollNotification,
color: const Color(0xFFABCDEF), child: Container(
child: ListView.builder( key: containerKey,
controller: scrollController, color: const Color(0xFFABCDEF),
itemExtent: itemExtent, child: ListView.builder(
itemCount: itemCount, controller: scrollController,
itemBuilder: (BuildContext context, int index) => Text('Item $index'), itemExtent: itemExtent,
itemCount: itemCount,
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
),
), ),
), );
); },
}, ),
), ],
], ),
), ),
); );
} }
......
...@@ -278,11 +278,11 @@ void main() { ...@@ -278,11 +278,11 @@ void main() {
RenderObject painter; RenderObject painter;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestScrollBehavior1(), behavior: TestScrollBehavior1(),
child: const CustomScrollView( child: CustomScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
reverse: true, reverse: true,
...@@ -300,11 +300,11 @@ void main() { ...@@ -300,11 +300,11 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpWidget( await tester.pumpWidget(
Directionality( const Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestScrollBehavior2(), behavior: TestScrollBehavior2(),
child: const CustomScrollView( child: CustomScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
...@@ -326,7 +326,7 @@ void main() { ...@@ -326,7 +326,7 @@ void main() {
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestScrollBehavior2(), behavior: const TestScrollBehavior2(),
child: CustomScrollView( child: CustomScrollView(
center: centerKey, center: centerKey,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
...@@ -365,7 +365,7 @@ void main() { ...@@ -365,7 +365,7 @@ void main() {
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestScrollBehavior2(), behavior: const TestScrollBehavior2(),
child: NotificationListener<OverscrollIndicatorNotification>( child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (OverscrollIndicatorNotification notification) { onNotification: (OverscrollIndicatorNotification notification) {
if (notification.leading) { if (notification.leading) {
...@@ -534,22 +534,26 @@ void main() { ...@@ -534,22 +534,26 @@ void main() {
} }
class TestScrollBehavior1 extends ScrollBehavior { class TestScrollBehavior1 extends ScrollBehavior {
const TestScrollBehavior1();
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
child: child, child: child,
axisDirection: axisDirection, axisDirection: details.direction,
color: const Color(0xFF00FF00), color: const Color(0xFF00FF00),
); );
} }
} }
class TestScrollBehavior2 extends ScrollBehavior { class TestScrollBehavior2 extends ScrollBehavior {
const TestScrollBehavior2();
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
child: child, child: child,
axisDirection: axisDirection, axisDirection: details.direction,
color: const Color(0xFF0000FF), color: const Color(0xFF0000FF),
); );
} }
......
...@@ -304,7 +304,12 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior { ...@@ -304,7 +304,12 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior {
TargetPlatform getPlatform(BuildContext context) => throw 'should not be called'; TargetPlatform getPlatform(BuildContext context) => throw 'should not be called';
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return child;
}
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
return child; return child;
} }
......
...@@ -19,6 +19,7 @@ Future<void> pumpTest( ...@@ -19,6 +19,7 @@ Future<void> pumpTest(
ScrollController? controller, ScrollController? controller,
}) async { }) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior(),
theme: ThemeData( theme: ThemeData(
platform: platform, platform: platform,
), ),
...@@ -34,6 +35,13 @@ Future<void> pumpTest( ...@@ -34,6 +35,13 @@ Future<void> pumpTest(
await tester.pump(const Duration(seconds: 5)); // to let the theme animate await tester.pump(const Duration(seconds: 5)); // to let the theme animate
} }
class NoScrollbarBehavior extends MaterialScrollBehavior {
const NoScrollbarBehavior();
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
}
// Pump a nested scrollable. The outer scrollable contains a sliver of a // Pump a nested scrollable. The outer scrollable contains a sliver of a
// 300-pixel-long scrollable followed by a 2000-pixel-long content. // 300-pixel-long scrollable followed by a 2000-pixel-long content.
Future<void> pumpDoubleScrollableTest( Future<void> pumpDoubleScrollableTest(
......
...@@ -32,11 +32,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate ...@@ -32,11 +32,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate
} }
class TestBehavior extends ScrollBehavior { class TestBehavior extends ScrollBehavior {
const TestBehavior();
@override @override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator( return GlowingOverscrollIndicator(
child: child, child: child,
axisDirection: axisDirection, axisDirection: details.direction,
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
); );
} }
...@@ -78,7 +80,7 @@ void main() { ...@@ -78,7 +80,7 @@ void main() {
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: TestBehavior(), behavior: const TestBehavior(),
child: Scrollbar( child: Scrollbar(
child: Scrollable( child: Scrollable(
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
......
...@@ -19,7 +19,7 @@ class FooState extends State<Foo> { ...@@ -19,7 +19,7 @@ class FooState extends State<Foo> {
return LayoutBuilder( return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return ScrollConfiguration( return ScrollConfiguration(
behavior: FooScrollBehavior(), behavior: const FooScrollBehavior(),
child: ListView( child: ListView(
controller: scrollController, controller: scrollController,
children: <Widget>[ children: <Widget>[
...@@ -74,6 +74,8 @@ class FooState extends State<Foo> { ...@@ -74,6 +74,8 @@ class FooState extends State<Foo> {
} }
class FooScrollBehavior extends ScrollBehavior { class FooScrollBehavior extends ScrollBehavior {
const FooScrollBehavior();
@override @override
bool shouldNotify(FooScrollBehavior old) => true; bool shouldNotify(FooScrollBehavior old) => true;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment