diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart index bad91917fa..22246bdfe1 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart @@ -168,6 +168,7 @@ class CupertinoApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.scrollBehavior, }) : assert(routes != null), assert(navigatorObservers != null), assert(title != null), @@ -207,6 +208,7 @@ class CupertinoApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.scrollBehavior, }) : assert(title != null), assert(showPerformanceOverlay != null), assert(checkerboardRasterCacheImages != null), @@ -393,6 +395,16 @@ class CupertinoApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.restorationScopeId} final String? restorationScopeId; + /// {@macro flutter.material.materialApp.scrollBehavior} + /// + /// When null, defaults to [CupertinoScrollBehavior]. + /// + /// See also: + /// + /// * [ScrollConfiguration], which controls how [Scrollable] widgets behave + /// in a subtree. + final ScrollBehavior? scrollBehavior; + @override _CupertinoAppState createState() => _CupertinoAppState(); @@ -403,7 +415,18 @@ class CupertinoApp extends StatefulWidget { HeroController(); // Linear tweening. } -class _AlwaysCupertinoScrollBehavior extends ScrollBehavior { +/// Describes how [Scrollable] widgets behave for [CupertinoApp]s. +/// +/// {@macro flutter.widgets.scrollBehavior} +/// +/// 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. +/// +/// See also: +/// +/// * [ScrollBehavior], the default scrolling behavior extended by this class. +class CupertinoScrollBehavior extends ScrollBehavior { @override Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { // Never build any overscroll glow indicators. @@ -521,7 +544,7 @@ class _CupertinoAppState extends State { final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData(); return ScrollConfiguration( - behavior: _AlwaysCupertinoScrollBehavior(), + behavior: widget.scrollBehavior ?? CupertinoScrollBehavior(), child: CupertinoUserInterfaceLevel( data: CupertinoUserInterfaceLevelData.base, child: CupertinoTheme( diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index d37549c166..1073d6ac86 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -196,6 +196,7 @@ class MaterialApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.scrollBehavior, }) : assert(routes != null), assert(navigatorObservers != null), assert(title != null), @@ -242,6 +243,7 @@ class MaterialApp extends StatefulWidget { this.shortcuts, this.actions, this.restorationScopeId, + this.scrollBehavior, }) : assert(routeInformationParser != null), assert(routerDelegate != null), assert(title != null), @@ -632,6 +634,23 @@ class MaterialApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.restorationScopeId} final String? restorationScopeId; + /// {@template flutter.material.materialApp.scrollBehavior} + /// The default [ScrollBehavior] for the application. + /// + /// [ScrollBehavior]s describe how [Scrollable] widgets behave. Providing + /// a [ScrollBehavior] can set the default [ScrollPhysics] across + /// an application, and manage [Scrollable] decorations like [Scrollbar]s and + /// [GlowingOverscrollIndicator]s. + /// {@endtemplate} + /// + /// When null, defaults to [MaterialScrollBehavior]. + /// + /// See also: + /// + /// * [ScrollConfiguration], which controls how [Scrollable] widgets behave + /// in a subtree. + final ScrollBehavior? scrollBehavior; + /// Turns on a [GridPaper] overlay that paints a baseline grid /// Material apps. /// @@ -657,7 +676,18 @@ class MaterialApp extends StatefulWidget { } } -class _MaterialScrollBehavior extends ScrollBehavior { +/// Describes how [Scrollable] widgets behave for [MaterialApp]s. +/// +/// {@macro flutter.widgets.scrollBehavior} +/// +/// Setting a [MaterialScrollBehavior] will apply a +/// [GlowingOverscrollIndicator] to [Scrollable] descendants when executing on +/// [TargetPlatform.android] and [TargetPlatform.fuchsia]. +/// +/// See also: +/// +/// * [ScrollBehavior], the default scrolling behavior extended by this class. +class MaterialScrollBehavior extends ScrollBehavior { @override TargetPlatform getPlatform(BuildContext context) { return Theme.of(context).platform; @@ -850,7 +880,7 @@ class _MaterialAppState extends State { }()); return ScrollConfiguration( - behavior: _MaterialScrollBehavior(), + behavior: widget.scrollBehavior ?? MaterialScrollBehavior(), child: HeroControllerScope( controller: _heroController, child: result, diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart index a857d666dc..c0fe9a5662 100644 --- a/packages/flutter/lib/src/widgets/scroll_configuration.dart +++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart @@ -14,8 +14,21 @@ const Color _kDefaultGlowColor = Color(0xFFFFFFFF); /// Describes how [Scrollable] widgets should behave. /// +/// {@template flutter.widgets.scrollBehavior} /// Used by [ScrollConfiguration] to configure the [Scrollable] widgets in a /// subtree. +/// +/// 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. +/// {@endtemplate} +/// +/// See also: +/// +/// * [ScrollConfiguration], the inherited widget that controls how +/// [Scrollable] widgets behave in a subtree. @immutable class ScrollBehavior { /// Creates a description of how [Scrollable] widgets should behave. @@ -33,7 +46,7 @@ class ScrollBehavior { /// overscrolls. Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { // When modifying this function, consider modifying the implementation in - // _MaterialScrollBehavior as well. + // MaterialScrollBehavior as well. switch (getPlatform(context)) { case TargetPlatform.iOS: case TargetPlatform.linux: diff --git a/packages/flutter/test/cupertino/app_test.dart b/packages/flutter/test/cupertino/app_test.dart index f37db85734..8e799005f5 100644 --- a/packages/flutter/test/cupertino/app_test.dart +++ b/packages/flutter/test/cupertino/app_test.dart @@ -178,6 +178,44 @@ void main() { await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); + + testWidgets('CupertinoApp has correct default ScrollBehavior', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + CupertinoApp( + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + expect(ScrollConfiguration.of(capturedContext).runtimeType, CupertinoScrollBehavior); + }); + + testWidgets('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + CupertinoApp( + scrollBehavior: MockScrollBehavior(), + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); + expect(scrollBehavior.runtimeType, MockScrollBehavior); + expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); + }); +} + +class MockScrollBehavior extends ScrollBehavior { + @override + ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation); diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 43543dab93..2be1a71903 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -1026,6 +1026,44 @@ void main() { )); expect(builderChild, isNull); }); + + testWidgets('MaterialApp has correct default ScrollBehavior', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + expect(ScrollConfiguration.of(capturedContext).runtimeType, MaterialScrollBehavior); + }); + + testWidgets('A ScrollBehavior can be set for MaterialApp', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + scrollBehavior: MockScrollBehavior(), + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); + expect(scrollBehavior.runtimeType, MockScrollBehavior); + expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); + }); +} + +class MockScrollBehavior extends ScrollBehavior { + @override + ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); } class MockAccessibilityFeature implements AccessibilityFeatures { diff --git a/packages/flutter/test/widgets/app_test.dart b/packages/flutter/test/widgets/app_test.dart index e5ea22089b..08320ab4fa 100644 --- a/packages/flutter/test/widgets/app_test.dart +++ b/packages/flutter/test/widgets/app_test.dart @@ -308,6 +308,25 @@ void main() { )); expect(find.text('/'), findsOneWidget); }); + + testWidgets('WidgetsApp has correct default ScrollBehavior', (WidgetTester tester) async { + late BuildContext capturedContext; + await tester.pumpWidget( + WidgetsApp( + builder: (BuildContext context, Widget? child) { + capturedContext = context; + return const Placeholder(); + }, + color: const Color(0xFF123456), + ), + ); + expect(ScrollConfiguration.of(capturedContext).runtimeType, ScrollBehavior); + }); +} + +class MockScrollBehavior extends ScrollBehavior { + @override + ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);