From 5517cc9b3b3bcf12431b47f495e342a30b738835 Mon Sep 17 00:00:00 2001 From: Enguerrand ARMINJON <37028599+EArminjon@users.noreply.github.com> Date: Wed, 15 Jan 2025 22:17:29 +0100 Subject: [PATCH] feat: Change default value of keyboardDismissBehavior (#158580) This PR aim to let developer choose the default ScrollViewKeyboardDismissBehavior value. I removed all default values of keyboardDismissBehavior and instead use the one from `ScrollConfiguration`, which use as default value `ScrollViewKeyboardDismissBehavior.manual`. This PR try to fix : https://github.com/flutter/flutter/issues/158566
Code Example ```dart import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', scrollBehavior: const MaterialScrollBehavior().copyWith( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, ), theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: TextField(), ), body: ListView.builder( itemCount: 100, itemBuilder: (context, index) => ListTile(title: Text('Item $index')), ), ); } } ```
https://github.com/user-attachments/assets/8341c3da-2685-4f55-b8e9-11d2aae907db ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Kate Lovett --- .../lib/src/material/reorderable_list.dart | 9 ++-- .../lib/src/widgets/reorderable_list.dart | 7 +-- .../lib/src/widgets/scroll_configuration.dart | 18 ++++++++ .../flutter/lib/src/widgets/scroll_view.dart | 17 +++++-- .../src/widgets/single_child_scroll_view.dart | 14 ++++-- .../widgets/two_dimensional_scroll_view.dart | 14 ++++-- packages/flutter/test/cupertino/app_test.dart | 45 +++++++++++++++++++ packages/flutter/test/material/app_test.dart | 45 +++++++++++++++++++ 8 files changed, 152 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index 0ec5f8b9f2..c2f4fd6bfd 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -98,7 +98,7 @@ class ReorderableListView extends StatefulWidget { this.anchor = 0.0, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, this.restorationId, this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, @@ -169,7 +169,7 @@ class ReorderableListView extends StatefulWidget { this.anchor = 0.0, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, this.restorationId, this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, @@ -271,8 +271,9 @@ class ReorderableListView extends StatefulWidget { /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} /// - /// The default is [ScrollViewKeyboardDismissBehavior.manual] - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// If [keyboardDismissBehavior] is null then it will fallback to the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; diff --git a/packages/flutter/lib/src/widgets/reorderable_list.dart b/packages/flutter/lib/src/widgets/reorderable_list.dart index c132c4f1f9..b1359507af 100644 --- a/packages/flutter/lib/src/widgets/reorderable_list.dart +++ b/packages/flutter/lib/src/widgets/reorderable_list.dart @@ -168,7 +168,7 @@ class ReorderableList extends StatefulWidget { this.anchor = 0.0, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, this.restorationId, this.clipBehavior = Clip.hardEdge, this.autoScrollerVelocityScalar, @@ -280,8 +280,9 @@ class ReorderableList extends StatefulWidget { /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} /// - /// The default is [ScrollViewKeyboardDismissBehavior.manual] - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// If [keyboardDismissBehavior] is null then it will fallback to the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart index 7cdc43fa1a..ddff43e4b9 100644 --- a/packages/flutter/lib/src/widgets/scroll_configuration.dart +++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart @@ -18,6 +18,7 @@ import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'framework.dart'; import 'overscroll_indicator.dart'; import 'scroll_physics.dart'; +import 'scroll_view.dart'; import 'scrollable.dart'; import 'scrollable_helpers.dart'; import 'scrollbar.dart'; @@ -89,6 +90,7 @@ class ScrollBehavior { Set? pointerAxisModifiers, ScrollPhysics? physics, TargetPlatform? platform, + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, }) { return _WrappedScrollBehavior( delegate: this, @@ -99,6 +101,7 @@ class ScrollBehavior { pointerAxisModifiers: pointerAxisModifiers, physics: physics, platform: platform, + keyboardDismissBehavior: keyboardDismissBehavior, ); } @@ -264,6 +267,12 @@ class ScrollBehavior { /// method returns false, the rebuilds might be optimized away. bool shouldNotify(covariant ScrollBehavior oldDelegate) => false; + /// The default keyboard dismissal behavior for [ScrollView] widgets. + /// + /// Defaults to [ScrollViewKeyboardDismissBehavior.manual]. + ScrollViewKeyboardDismissBehavior getKeyboardDismissBehavior(BuildContext context) => + ScrollViewKeyboardDismissBehavior.manual; + @override String toString() => objectRuntimeType(this, 'ScrollBehavior'); } @@ -278,6 +287,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { Set? pointerAxisModifiers, this.physics, this.platform, + this.keyboardDismissBehavior, }) : _dragDevices = dragDevices, _pointerAxisModifiers = pointerAxisModifiers; @@ -286,6 +296,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { final bool overscroll; final ScrollPhysics? physics; final TargetPlatform? platform; + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; final Set? _dragDevices; final MultitouchDragStrategy? multitouchDragStrategy; final Set? _pointerAxisModifiers; @@ -327,6 +338,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { Set? pointerAxisModifiers, ScrollPhysics? physics, TargetPlatform? platform, + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, }) { return delegate.copyWith( scrollbars: scrollbars ?? this.scrollbars, @@ -336,6 +348,7 @@ class _WrappedScrollBehavior implements ScrollBehavior { pointerAxisModifiers: pointerAxisModifiers ?? this.pointerAxisModifiers, physics: physics ?? this.physics, platform: platform ?? this.platform, + keyboardDismissBehavior: keyboardDismissBehavior ?? this.keyboardDismissBehavior, ); } @@ -349,6 +362,11 @@ class _WrappedScrollBehavior implements ScrollBehavior { return physics ?? delegate.getScrollPhysics(context); } + @override + ScrollViewKeyboardDismissBehavior getKeyboardDismissBehavior(BuildContext context) { + return keyboardDismissBehavior ?? delegate.getKeyboardDismissBehavior(context); + } + @override bool shouldNotify(_WrappedScrollBehavior oldDelegate) { return oldDelegate.delegate.runtimeType != delegate.runtimeType || diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index 1addb29090..9d63d815cf 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -118,7 +118,7 @@ abstract class ScrollView extends StatelessWidget { this.cacheExtent, this.semanticChildCount, this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, @@ -374,10 +374,14 @@ abstract class ScrollView extends StatelessWidget { final DragStartBehavior dragStartBehavior; /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} - /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// The [ScrollViewKeyboardDismissBehavior] defines how this [ScrollView] will /// dismiss the keyboard automatically. /// {@endtemplate} - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// + /// If [keyboardDismissBehavior] is null then it will fallback to + /// [scrollBehavior]. If that is also null, the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior] will be used. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; @@ -505,7 +509,12 @@ abstract class ScrollView extends StatelessWidget { ? PrimaryScrollController.none(child: scrollable) : scrollable; - if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { + final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = + keyboardDismissBehavior ?? + scrollBehavior?.getKeyboardDismissBehavior(context) ?? + ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); + + if (effectiveKeyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { return NotificationListener( child: scrollableResult, onNotification: (ScrollUpdateNotification notification) { diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart index a34611a9dd..b3968991f2 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -18,6 +18,7 @@ import 'focus_scope.dart'; import 'framework.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'; @@ -158,7 +159,7 @@ class SingleChildScrollView extends StatelessWidget { this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.restorationId, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, }) : assert( !(controller != null && (primary ?? false)), 'Primary ScrollViews obtain their ScrollController via inheritance ' @@ -233,7 +234,10 @@ class SingleChildScrollView extends StatelessWidget { final String? restorationId; /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// + /// If [keyboardDismissBehavior] is null then it will fallback to the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); @@ -271,7 +275,11 @@ class SingleChildScrollView extends StatelessWidget { }, ); - if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { + final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = + keyboardDismissBehavior ?? + ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); + + if (effectiveKeyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { scrollable = NotificationListener( child: scrollable, onNotification: (ScrollUpdateNotification notification) { diff --git a/packages/flutter/lib/src/widgets/two_dimensional_scroll_view.dart b/packages/flutter/lib/src/widgets/two_dimensional_scroll_view.dart index 174d55b366..c4f450c841 100644 --- a/packages/flutter/lib/src/widgets/two_dimensional_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/two_dimensional_scroll_view.dart @@ -13,6 +13,7 @@ import 'focus_scope.dart'; import 'framework.dart'; import 'notification_listener.dart'; import 'primary_scroll_controller.dart'; +import 'scroll_configuration.dart'; import 'scroll_controller.dart'; import 'scroll_delegate.dart'; import 'scroll_notification.dart'; @@ -63,7 +64,7 @@ abstract class TwoDimensionalScrollView extends StatelessWidget { this.cacheExtent, this.diagonalDragBehavior = DiagonalDragBehavior.none, this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.keyboardDismissBehavior, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, }); @@ -109,7 +110,10 @@ abstract class TwoDimensionalScrollView extends StatelessWidget { final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} - final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// + /// If [keyboardDismissBehavior] is null then it will fallback to the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior]. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.hitTestBehavior} /// @@ -187,7 +191,11 @@ abstract class TwoDimensionalScrollView extends StatelessWidget { ? PrimaryScrollController.none(child: scrollable) : scrollable; - if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { + final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = + keyboardDismissBehavior ?? + ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); + + if (effectiveKeyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { return NotificationListener( child: scrollableResult, onNotification: (ScrollUpdateNotification notification) { diff --git a/packages/flutter/test/cupertino/app_test.dart b/packages/flutter/test/cupertino/app_test.dart index 748bd8f6ce..6e99392591 100644 --- a/packages/flutter/test/cupertino/app_test.dart +++ b/packages/flutter/test/cupertino/app_test.dart @@ -392,6 +392,51 @@ void main() { ); }); + testWidgets('CupertinoApp has correct default KeyboardDismissBehavior', ( + WidgetTester tester, + ) async { + late BuildContext capturedContext; + await tester.pumpWidget( + CupertinoApp( + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + + expect( + ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), + ScrollViewKeyboardDismissBehavior.manual, + ); + }); + + testWidgets('CupertinoApp can override default KeyboardDismissBehavior', ( + WidgetTester tester, + ) async { + late BuildContext capturedContext; + await tester.pumpWidget( + CupertinoApp( + scrollBehavior: const CupertinoScrollBehavior().copyWith( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + ), + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + + expect( + ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), + ScrollViewKeyboardDismissBehavior.onDrag, + ); + }); + testWidgets('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 996d5c38bb..828017deec 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -1248,6 +1248,51 @@ void main() { expect(ScrollConfiguration.of(capturedContext).runtimeType, MaterialScrollBehavior); }); + testWidgets('MaterialApp has correct default KeyboardDismissBehavior', ( + WidgetTester tester, + ) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + + expect( + ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), + ScrollViewKeyboardDismissBehavior.manual, + ); + }); + + testWidgets('MaterialApp can override default KeyboardDismissBehavior', ( + WidgetTester tester, + ) async { + late BuildContext capturedContext; + await tester.pumpWidget( + MaterialApp( + scrollBehavior: const MaterialScrollBehavior().copyWith( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + ), + home: Builder( + builder: (BuildContext context) { + capturedContext = context; + return const Placeholder(); + }, + ), + ), + ); + + expect( + ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), + ScrollViewKeyboardDismissBehavior.onDrag, + ); + }); + testWidgets('A ScrollBehavior can be set for MaterialApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget(