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(