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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button.dart';
......@@ -10,6 +11,7 @@ import 'icons.dart';
import 'interface_level.dart';
import 'localizations.dart';
import 'route.dart';
import 'scrollbar.dart';
import 'theme.dart';
/// An application that uses Cupertino design.
......@@ -422,14 +424,40 @@ class CupertinoApp extends StatefulWidget {
/// Setting a [CupertinoScrollBehavior] will result in descendant [Scrollable] widgets
/// using [BouncingScrollPhysics] by default. No [GlowingOverscrollIndicator] is
/// applied when using a [CupertinoScrollBehavior] either, regardless of platform.
/// When executing on desktop platforms, a [CupertinoScrollbar] is applied to the child.
///
/// See also:
///
/// * [ScrollBehavior], the default scrolling behavior extended by this class.
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
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
// Never build any overscroll glow indicators.
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
// No overscroll indicator.
// When modifying this function, consider modifying the implementation in
// the base class as well.
return child;
}
......@@ -544,7 +572,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
return ScrollConfiguration(
behavior: widget.scrollBehavior ?? CupertinoScrollBehavior(),
behavior: widget.scrollBehavior ?? const CupertinoScrollBehavior(),
child: CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme(
......
......@@ -14,6 +14,7 @@ import 'icons.dart';
import 'material_localizations.dart';
import 'page.dart';
import 'scaffold.dart' show ScaffoldMessenger, ScaffoldMessengerState;
import 'scrollbar.dart';
import 'theme.dart';
/// [MaterialApp] uses this [TextStyle] as its [DefaultTextStyle] to encourage
......@@ -684,17 +685,47 @@ class MaterialApp extends StatefulWidget {
/// [GlowingOverscrollIndicator] to [Scrollable] descendants when executing on
/// [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:
///
/// * [ScrollBehavior], the default scrolling behavior extended by this class.
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
TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform;
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
// 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
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
// the base class as well.
switch (getPlatform(context)) {
......@@ -707,7 +738,7 @@ class MaterialScrollBehavior extends ScrollBehavior {
case TargetPlatform.fuchsia:
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
axisDirection: details.direction,
color: Theme.of(context).colorScheme.secondary,
);
}
......@@ -880,7 +911,7 @@ class _MaterialAppState extends State<MaterialApp> {
}());
return ScrollConfiguration(
behavior: widget.scrollBehavior ?? MaterialScrollBehavior(),
behavior: widget.scrollBehavior ?? const MaterialScrollBehavior(),
child: HeroControllerScope(
controller: _heroController,
child: result,
......
......@@ -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.
class _DropdownMenuItemButton<T> extends StatefulWidget {
const _DropdownMenuItemButton({
......@@ -289,7 +274,14 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
type: MaterialType.transparency,
textStyle: route.style,
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(
controller: widget.route.scrollController!,
child: Scrollbar(
......
......@@ -23,6 +23,7 @@ import 'focus_scope.dart';
import 'framework.dart';
import 'localizations.dart';
import 'media_query.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
......@@ -493,6 +494,7 @@ class EditableText extends StatefulWidget {
this.autofillHints,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
......@@ -1201,6 +1203,10 @@ class EditableText extends StatefulWidget {
///
/// See [Scrollable.physics].
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [scrollPhysics].
final ScrollPhysics? scrollPhysics;
/// {@template flutter.widgets.editableText.selectionEnabled}
......@@ -1305,6 +1311,23 @@ class EditableText extends StatefulWidget {
/// Flutter.
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.
static TextInputType _inferKeyboardType({
required Iterable<String>? autofillHints,
......@@ -2609,6 +2632,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
physics: widget.scrollPhysics,
dragStartBehavior: widget.dragStartBehavior,
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) {
return CompositedTransformTarget(
link: _toolbarLayerLink,
......
......@@ -12,6 +12,7 @@ import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
......@@ -431,6 +432,7 @@ class _FixedExtentScrollable extends Scrollable {
required this.itemExtent,
required ViewportBuilder viewportBuilder,
String? restorationId,
ScrollBehavior? scrollBehavior,
}) : super (
key: key,
axisDirection: axisDirection,
......@@ -438,6 +440,7 @@ class _FixedExtentScrollable extends Scrollable {
physics: physics,
viewportBuilder: viewportBuilder,
restorationId: restorationId,
scrollBehavior: scrollBehavior,
);
final double itemExtent;
......@@ -584,6 +587,7 @@ class ListWheelScrollView extends StatefulWidget {
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
required List<Widget> children,
}) : assert(children != null),
assert(diameterRatio != null),
......@@ -625,6 +629,7 @@ class ListWheelScrollView extends StatefulWidget {
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
required this.childDelegate,
}) : assert(childDelegate != null),
assert(diameterRatio != null),
......@@ -669,6 +674,10 @@ class ListWheelScrollView extends StatefulWidget {
/// For example, determines how the scroll view continues to animate after the
/// 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.
final ScrollPhysics? physics;
......@@ -716,6 +725,17 @@ class ListWheelScrollView extends StatefulWidget {
/// {@macro flutter.widgets.scrollable.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
_ListWheelScrollViewState createState() => _ListWheelScrollViewState();
}
......@@ -769,6 +789,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
physics: widget.physics,
itemExtent: widget.itemExtent,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return ListWheelViewport(
diameterRatio: widget.diameterRatio,
......
......@@ -13,6 +13,7 @@ import 'basic.dart';
import 'framework.dart';
import 'primary_scroll_controller.dart';
import 'scroll_activity.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
......@@ -369,6 +370,7 @@ class NestedScrollView extends StatefulWidget {
this.floatHeaderSlivers = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
this.scrollBehavior,
}) : assert(scrollDirection != null),
assert(reverse != null),
assert(headerSliverBuilder != null),
......@@ -407,6 +409,10 @@ class NestedScrollView extends StatefulWidget {
/// [ScrollPhysics.createBallisticSimulation] allows this particular aspect of
/// 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.
///
/// The [ScrollPhysics.applyBoundaryConditions] implementation of the provided
......@@ -453,6 +459,20 @@ class NestedScrollView extends StatefulWidget {
/// {@macro flutter.widgets.scrollable.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
/// [NestedScrollView].
///
......@@ -613,6 +633,10 @@ class NestedScrollViewState extends State<NestedScrollView> {
@override
Widget build(BuildContext context) {
final ScrollPhysics _scrollPhysics = widget.physics?.applyTo(const ClampingScrollPhysics())
?? widget.scrollBehavior?.getScrollPhysics(context).applyTo(const ClampingScrollPhysics())
?? const ClampingScrollPhysics();
return _InheritedNestedScrollView(
state: this,
child: Builder(
......@@ -622,9 +646,8 @@ class NestedScrollViewState extends State<NestedScrollView> {
dragStartBehavior: widget.dragStartBehavior,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
physics: widget.physics != null
? widget.physics!.applyTo(const ClampingScrollPhysics())
: const ClampingScrollPhysics(),
physics: _scrollPhysics,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
controller: _coordinator!._outerController,
slivers: widget._buildSlivers(
context,
......@@ -646,6 +669,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
required Axis scrollDirection,
required bool reverse,
required ScrollPhysics physics,
required ScrollBehavior scrollBehavior,
required ScrollController controller,
required List<Widget> slivers,
required this.handle,
......@@ -656,6 +680,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
scrollDirection: scrollDirection,
reverse: reverse,
physics: physics,
scrollBehavior: scrollBehavior,
controller: controller,
slivers: slivers,
dragStartBehavior: dragStartBehavior,
......
......@@ -27,7 +27,7 @@ import 'ticker_provider.dart';
/// showing the indication, call [OverscrollIndicatorNotification.disallowGlow]
/// 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.
///
/// In a [MaterialApp], the edge glow color is the overall theme's
......@@ -189,7 +189,7 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// subtree) should include a source of [ScrollNotification] notifications.
///
/// 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.
final Widget? child;
......
......@@ -13,6 +13,7 @@ import 'debug.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
import 'scroll_metrics.dart';
......@@ -635,6 +636,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
controller = controller ?? _defaultPageController,
......@@ -673,6 +675,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
controller = controller ?? _defaultPageController,
......@@ -776,6 +779,7 @@ class PageView extends StatefulWidget {
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
}) : assert(childrenDelegate != null),
assert(allowImplicitScrolling != null),
assert(clipBehavior != null),
......@@ -829,6 +833,10 @@ class PageView extends StatefulWidget {
/// The physics are modified to snap to page boundaries using
/// [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.
final ScrollPhysics? physics;
......@@ -854,6 +862,17 @@ class PageView extends StatefulWidget {
/// Defaults to [Clip.hardEdge].
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
_PageViewState createState() => _PageViewState();
}
......@@ -885,8 +904,8 @@ class _PageViewState extends State<PageView> {
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics)
: widget.physics);
? _kPagePhysics.applyTo(widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context))
: widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context));
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
......@@ -906,6 +925,7 @@ class _PageViewState extends State<PageView> {
controller: widget.controller,
physics: physics,
restorationId: widget.restorationId,
scrollBehavior: widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false),
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
......
......@@ -9,6 +9,8 @@ import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'overscroll_indicator.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
import 'scrollbar.dart';
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
/// subtree. For example, overriding [ScrollBehavior.getScrollPhysics] sets the
/// default [ScrollPhysics] for [Scrollable]s that inherit this [ScrollConfiguration].
/// Overriding [ScrollBehavior.buildViewportChrome] can be used to add or change
/// default decorations like [GlowingOverscrollIndicator]s.
/// Overriding [ScrollBehavior.buildOverscrollIndicator] can be used to add or change
/// the default [GlowingOverscrollIndicator] decoration, while
/// [ScrollBehavior.buildScrollbar] can be changed to modify the default [Scrollbar].
///
/// When looking to easily toggle the default decorations, you can use
/// [ScrollBehavior.copyWith] instead of creating your own [ScrollBehavior] class.
/// The `scrollbar` and `overscrollIndicator` flags can turn these decorations off.
/// {@endtemplate}
///
/// See also:
......@@ -34,6 +41,29 @@ class ScrollBehavior {
/// Creates a description of how [Scrollable] widgets should behave.
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.
///
/// Defaults to the current platform.
......@@ -44,9 +74,14 @@ class ScrollBehavior {
/// For example, on Android, this method wraps the given widget with a
/// [GlowingOverscrollIndicator] to provide visual feedback when the user
/// 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) {
// When modifying this function, consider modifying the implementation in
// MaterialScrollBehavior as well.
switch (getPlatform(context)) {
case TargetPlatform.iOS:
case TargetPlatform.linux:
......@@ -55,14 +90,43 @@ class ScrollBehavior {
return child;
case TargetPlatform.android:
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,
axisDirection: axisDirection,
color: _kDefaultGlowColor,
controller: details.controller,
);
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
/// [Scrollable]s' drag gesture recognizers, for estimating the velocity of a
/// drag gesture.
......@@ -129,6 +193,84 @@ class 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.
///
/// The scroll configuration determines the [ScrollPhysics] and viewport
......
......@@ -24,7 +24,7 @@ abstract class ScrollContext {
/// This context is typically different that the context of the scrollable
/// widget itself. For example, [Scrollable] uses a context outside the
/// [Viewport] but inside the widgets created by
/// [ScrollBehavior.buildViewportChrome].
/// [ScrollBehavior.buildOverscrollIndicator] and [ScrollBehavior.buildScrollbar].
BuildContext? get notificationContext;
/// The [BuildContext] that should be used when searching for a [PageStorage].
......
......@@ -15,6 +15,7 @@ import 'framework.dart';
import 'media_query.dart';
import 'notification_listener.dart';
import 'primary_scroll_controller.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
......@@ -83,6 +84,7 @@ abstract class ScrollView extends StatelessWidget {
this.controller,
bool? primary,
ScrollPhysics? physics,
this.scrollBehavior,
this.shrinkWrap = false,
this.center,
this.anchor = 0.0,
......@@ -205,8 +207,20 @@ abstract class ScrollView extends StatelessWidget {
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [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}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
......@@ -380,6 +394,7 @@ abstract class ScrollView extends StatelessWidget {
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
scrollBehavior: scrollBehavior,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
......@@ -610,6 +625,7 @@ class CustomScrollView extends ScrollView {
ScrollController? controller,
bool? primary,
ScrollPhysics? physics,
ScrollBehavior? scrollBehavior,
bool shrinkWrap = false,
Key? center,
double anchor = 0.0,
......@@ -627,6 +643,7 @@ class CustomScrollView extends ScrollView {
controller: controller,
primary: primary,
physics: physics,
scrollBehavior: scrollBehavior,
shrinkWrap: shrinkWrap,
center: center,
anchor: anchor,
......
......@@ -25,7 +25,6 @@ import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'ticker_provider.dart';
import 'viewport.dart';
......@@ -91,6 +90,7 @@ class Scrollable extends StatefulWidget {
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.restorationId,
this.scrollBehavior,
}) : assert(axisDirection != null),
assert(dragStartBehavior != null),
assert(viewportBuilder != null),
......@@ -135,6 +135,10 @@ class Scrollable extends StatefulWidget {
/// Defaults to matching platform conventions via the physics provided from
/// 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
/// effect if the _class_ of the provided object changes. Merely constructing
/// a new instance with a different configuration is insufficient to cause the
......@@ -243,6 +247,14 @@ class Scrollable extends StatefulWidget {
/// {@endtemplate}
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.
///
/// Determined by the [axisDirection].
......@@ -385,27 +397,31 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
late ScrollBehavior _configuration;
ScrollPhysics? _physics;
ScrollController? _fallbackScrollController;
ScrollController get _effectiveScrollController => widget.controller ?? _fallbackScrollController!;
// Only call this from places that will definitely trigger a rebuild.
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null)
if (widget.physics != null) {
_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;
if (oldPosition != null) {
controller?.detach(oldPosition);
_effectiveScrollController.detach(oldPosition);
// 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
// position. So, schedule a microtask to do it.
scheduleMicrotask(oldPosition.dispose);
}
_position = controller?.createScrollPosition(_physics!, this, oldPosition)
?? ScrollPositionWithSingleContext(physics: _physics!, context: this, oldPosition: oldPosition);
_position = _effectiveScrollController.createScrollPosition(_physics!, this, oldPosition);
assert(_position != null);
controller?.attach(position);
_effectiveScrollController.attach(position);
}
@override
......@@ -426,6 +442,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
ServicesBinding.instance!.restorationManager.flushData();
}
@override
void initState() {
if (widget.controller == null)
_fallbackScrollController = ScrollController();
super.initState();
}
@override
void didChangeDependencies() {
_updatePosition();
......@@ -433,8 +456,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
}
bool _shouldUpdatePosition(Scrollable oldWidget) {
ScrollPhysics? newPhysics = widget.physics;
ScrollPhysics? oldPhysics = oldWidget.physics;
ScrollPhysics? newPhysics = widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context);
ScrollPhysics? oldPhysics = oldWidget.physics ?? oldWidget.scrollBehavior?.getScrollPhysics(context);
do {
if (newPhysics?.runtimeType != oldPhysics?.runtimeType)
return true;
......@@ -450,8 +473,25 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller?.detach(position);
widget.controller?.attach(position);
if (oldWidget.controller == null) {
// 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))
......@@ -460,7 +500,13 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
@override
void dispose() {
widget.controller?.detach(position);
if (widget.controller != null) {
widget.controller!.detach(position);
} else {
_fallbackScrollController?.detach(position);
_fallbackScrollController?.dispose();
}
position.dispose();
_persistedScrollOffset.dispose();
super.dispose();
......@@ -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
......@@ -731,6 +786,33 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
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
/// excluded from the scrollable area for semantics purposes.
///
......
......@@ -584,7 +584,8 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
///
/// If the scrollbar is wrapped around multiple [ScrollView]s, it only responds to
/// 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
/// a [controller] is not set. Scrollbar thumbs can be dragged along the main axis
......@@ -596,6 +597,17 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// painted. In this case, the scrollbar cannot accurately represent the
/// relative location of the visible area, or calculate the accurate delta to
/// 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}
///
// TODO(Piinks): Add code sample
......@@ -615,8 +627,8 @@ class RawScrollbar extends StatefulWidget {
/// The [child], or a descendant of the [child], should be a source of
/// [ScrollNotification] notifications, typically a [Scrollable] widget.
///
/// The [child], [thickness], [thumbColor], [isAlwaysShown], [fadeDuration],
/// and [timeToFade] arguments must not be null.
/// The [child], [fadeDuration], [pressDuration], and [timeToFade] arguments
/// must not be null.
const RawScrollbar({
Key? key,
required this.child,
......@@ -641,6 +653,9 @@ class RawScrollbar extends StatefulWidget {
///
/// The scrollbar will be stacked on top of this child. This child (and its
/// 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].
/// {@endtemplate}
......
......@@ -198,7 +198,7 @@ void main() {
late BuildContext capturedContext;
await tester.pumpWidget(
CupertinoApp(
scrollBehavior: MockScrollBehavior(),
scrollBehavior: const MockScrollBehavior(),
home: Builder(
builder: (BuildContext context) {
capturedContext = context;
......@@ -214,6 +214,8 @@ void main() {
}
class MockScrollBehavior extends ScrollBehavior {
const MockScrollBehavior();
@override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
}
......
......@@ -956,7 +956,6 @@ void main() {
},
);
testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async {
await tester.pumpWidget(
WidgetsApp(
......
......@@ -1046,7 +1046,7 @@ void main() {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
scrollBehavior: MockScrollBehavior(),
scrollBehavior: const MockScrollBehavior(),
home: Builder(
builder: (BuildContext context) {
capturedContext = context;
......@@ -1062,6 +1062,8 @@ void main() {
}
class MockScrollBehavior extends ScrollBehavior {
const MockScrollBehavior();
@override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
}
......
......@@ -49,11 +49,21 @@ Widget _buildBoilerplate({
textDirection: textDirection,
child: MediaQuery(
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() {
testWidgets("Scrollbar doesn't show when tapping list", (WidgetTester tester) async {
await tester.pumpWidget(
......@@ -809,18 +819,11 @@ void main() {
});
testWidgets('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: PrimaryScrollController(
controller: scrollController,
child: Scrollbar(
isAlwaysShown: true,
controller: scrollController,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(isAlwaysShown: true)),
home: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
);
......@@ -858,24 +861,18 @@ void main() {
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Hover animation is not triggered by tap gestures', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: PrimaryScrollController(
controller: scrollController,
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(
isAlwaysShown: true,
showTrackOnHover: true,
)),
home: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
);
......@@ -936,27 +933,19 @@ void main() {
color: const Color(0x80000000),
),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
}),
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets('Scrollbar showTrackOnHover', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: PrimaryScrollController(
controller: scrollController,
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(
isAlwaysShown: true,
showTrackOnHover: true,
)),
home: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0)
),
),
);
......@@ -1006,7 +995,6 @@ void main() {
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
......@@ -1088,33 +1076,36 @@ void main() {
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Scrollbar(
key: key2,
notificationPredicate: null,
child: SingleChildScrollView(
key: outerKey,
child: SizedBox(
height: 1000.0,
width: double.infinity,
child: Column(
children: <Widget>[
Scrollbar(
key: key1,
notificationPredicate: null,
child: SizedBox(
height: 300.0,
width: double.infinity,
child: SingleChildScrollView(
key: innerKey,
child: const SizedBox(
key: Key('Inner scrollable'),
height: 1000.0,
width: double.infinity,
child: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
key: key2,
notificationPredicate: null,
child: SingleChildScrollView(
key: outerKey,
child: SizedBox(
height: 1000.0,
width: double.infinity,
child: Column(
children: <Widget>[
Scrollbar(
key: key1,
notificationPredicate: null,
child: SizedBox(
height: 300.0,
width: double.infinity,
child: SingleChildScrollView(
key: innerKey,
child: const SizedBox(
key: Key('Inner scrollable'),
height: 1000.0,
width: double.infinity,
),
),
),
),
),
],
],
),
),
),
),
......@@ -1206,11 +1197,7 @@ void main() {
await tester.pumpAndSettle();
// The offset should not have changed.
expect(scrollController.offset, scrollAmount);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgets('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
......
......@@ -29,13 +29,16 @@ void main() {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
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() {
final ScrollbarThemeData scrollbarTheme = _scrollbarTheme();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(scrollbarTheme: scrollbarTheme),
home: Scrollbar(
isAlwaysShown: true,
controller: scrollController,
child: SingleChildScrollView(
theme: ThemeData(
scrollbarTheme: scrollbarTheme,
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
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() {
color: _kDefaultIdleThumbColor,
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgets('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
......@@ -298,12 +301,7 @@ void main() {
color: _kDefaultIdleThumbColor,
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async {
const double thickness = 4.0;
......@@ -314,17 +312,22 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light()),
home: Scrollbar(
thickness: thickness,
hoverThickness: hoverThickness,
isAlwaysShown: true,
showTrackOnHover: showTrackOnHover,
radius: radius,
controller: scrollController,
child: SingleChildScrollView(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
thickness: thickness,
hoverThickness: hoverThickness,
isAlwaysShown: true,
showTrackOnHover: showTrackOnHover,
radius: radius,
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() {
final ScrollController scrollController = ScrollController();
return MaterialApp(
theme: appTheme,
home: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
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() {
// Scrollbar defaults for light themes:
// - 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();
// Idle scrollbar behavior
expect(
......@@ -493,7 +501,9 @@ void main() {
// Scrollbar defaults for dark themes:
// - 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
// Idle scrollbar behavior
......@@ -617,6 +627,13 @@ void main() {
}, skip: kIsWeb);
}
class NoScrollbarBehavior extends ScrollBehavior {
const NoScrollbarBehavior();
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
}
ScrollbarThemeData _scrollbarTheme({
MaterialStateProperty<double?>? thickness,
bool showTrackOnHover = true,
......
......@@ -1788,24 +1788,27 @@ void main() {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GridView(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3,
mainAxisSpacing: 3,
crossAxisSpacing: 3),
children: const <Widget>[
Text('a'),
Text('b'),
Text('c'),
],
),
],
child: MediaQuery(
data: const MediaQueryData(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GridView(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 3,
mainAxisSpacing: 3,
crossAxisSpacing: 3),
children: const <Widget>[
Text('a'),
Text('b'),
Text('c'),
],
),
],
),
),
),
);
......
......@@ -327,11 +327,6 @@ void main() {
});
}
class MockScrollBehavior extends ScrollBehavior {
@override
ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics();
}
typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
......
......@@ -18,33 +18,36 @@ void main() {
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
TextButton(
child: const Text('TapHere'),
onPressed: onButtonPressed,
),
DraggableScrollableSheet(
maxChildSize: maxChildSize,
minChildSize: minChildSize,
initialChildSize: initialChildSize,
builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: Container(
key: containerKey,
color: const Color(0xFFABCDEF),
child: ListView.builder(
controller: scrollController,
itemExtent: itemExtent,
itemCount: itemCount,
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
TextButton(
child: const Text('TapHere'),
onPressed: onButtonPressed,
),
DraggableScrollableSheet(
maxChildSize: maxChildSize,
minChildSize: minChildSize,
initialChildSize: initialChildSize,
builder: (BuildContext context, ScrollController scrollController) {
return NotificationListener<ScrollNotification>(
onNotification: onScrollNotification,
child: Container(
key: containerKey,
color: const Color(0xFFABCDEF),
child: ListView.builder(
controller: scrollController,
itemExtent: itemExtent,
itemCount: itemCount,
itemBuilder: (BuildContext context, int index) => Text('Item $index'),
),
),
),
);
},
),
],
);
},
),
],
),
),
);
}
......
......@@ -278,11 +278,11 @@ void main() {
RenderObject painter;
await tester.pumpWidget(
Directionality(
const Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: TestScrollBehavior1(),
child: const CustomScrollView(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
physics: AlwaysScrollableScrollPhysics(),
reverse: true,
......@@ -300,11 +300,11 @@ void main() {
await tester.pumpAndSettle(const Duration(seconds: 1));
await tester.pumpWidget(
Directionality(
const Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: TestScrollBehavior2(),
child: const CustomScrollView(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
physics: AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
......@@ -326,7 +326,7 @@ void main() {
Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: TestScrollBehavior2(),
behavior: const TestScrollBehavior2(),
child: CustomScrollView(
center: centerKey,
physics: const AlwaysScrollableScrollPhysics(),
......@@ -365,7 +365,7 @@ void main() {
Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: TestScrollBehavior2(),
behavior: const TestScrollBehavior2(),
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (OverscrollIndicatorNotification notification) {
if (notification.leading) {
......@@ -534,22 +534,26 @@ void main() {
}
class TestScrollBehavior1 extends ScrollBehavior {
const TestScrollBehavior1();
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
axisDirection: details.direction,
color: const Color(0xFF00FF00),
);
}
}
class TestScrollBehavior2 extends ScrollBehavior {
const TestScrollBehavior2();
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
axisDirection: details.direction,
color: const Color(0xFF0000FF),
);
}
......
......@@ -304,7 +304,12 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior {
TargetPlatform getPlatform(BuildContext context) => throw 'should not be called';
@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;
}
......
......@@ -19,6 +19,7 @@ Future<void> pumpTest(
ScrollController? controller,
}) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior(),
theme: ThemeData(
platform: platform,
),
......@@ -34,6 +35,13 @@ Future<void> pumpTest(
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
// 300-pixel-long scrollable followed by a 2000-pixel-long content.
Future<void> pumpDoubleScrollableTest(
......
......@@ -32,11 +32,13 @@ class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate
}
class TestBehavior extends ScrollBehavior {
const TestBehavior();
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
axisDirection: details.direction,
color: const Color(0xFFFFFFFF),
);
}
......@@ -78,7 +80,7 @@ void main() {
child: Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: TestBehavior(),
behavior: const TestBehavior(),
child: Scrollbar(
child: Scrollable(
axisDirection: AxisDirection.down,
......
......@@ -19,7 +19,7 @@ class FooState extends State<Foo> {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return ScrollConfiguration(
behavior: FooScrollBehavior(),
behavior: const FooScrollBehavior(),
child: ListView(
controller: scrollController,
children: <Widget>[
......@@ -74,6 +74,8 @@ class FooState extends State<Foo> {
}
class FooScrollBehavior extends ScrollBehavior {
const FooScrollBehavior();
@override
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