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 c5ab2db9c5..bb9c79a66b 100644 --- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart @@ -4,14 +4,19 @@ import 'dart:math' as math; -import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; +import 'package:flutter/rendering.dart'; import 'basic.dart'; +import 'focus_manager.dart'; +import 'focus_scope.dart'; import 'framework.dart'; +import 'notification_listener.dart'; import 'primary_scroll_controller.dart'; import 'scroll_controller.dart'; +import 'scroll_notification.dart'; import 'scroll_physics.dart'; +import 'scroll_view.dart'; import 'scrollable.dart'; /// A box in which a single widget can be scrolled. @@ -221,6 +226,7 @@ class SingleChildScrollView extends StatelessWidget { this.dragStartBehavior = DragStartBehavior.start, this.clipBehavior = Clip.hardEdge, this.restorationId, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual }) : assert(scrollDirection != null), assert(dragStartBehavior != null), assert(clipBehavior != null), @@ -306,6 +312,9 @@ class SingleChildScrollView extends StatelessWidget { /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; + /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); } @@ -319,7 +328,7 @@ class SingleChildScrollView extends StatelessWidget { final ScrollController? scrollController = primary ? PrimaryScrollController.of(context) : controller; - final Scrollable scrollable = Scrollable( + Widget scrollable = Scrollable( dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, @@ -334,6 +343,20 @@ class SingleChildScrollView extends StatelessWidget { ); }, ); + + if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) { + scrollable = NotificationListener( + child: scrollable, + onNotification: (ScrollUpdateNotification notification) { + final FocusScopeNode focusNode = FocusScope.of(context); + if (notification.dragDetails != null && focusNode.hasFocus) { + focusNode.unfocus(); + } + return false; + }, + ); + } + return primary && scrollController != null ? PrimaryScrollController.none(child: scrollable) : scrollable; diff --git a/packages/flutter/test/widgets/single_child_scroll_view_test.dart b/packages/flutter/test/widgets/single_child_scroll_view_test.dart index 4243c3612f..88414f595d 100644 --- a/packages/flutter/test/widgets/single_child_scroll_view_test.dart +++ b/packages/flutter/test/widgets/single_child_scroll_view_test.dart @@ -4,10 +4,9 @@ import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - import '../rendering/rendering_tester.dart'; import 'semantics_tester.dart'; @@ -922,4 +921,53 @@ void main() { expect(inner.offset, 0.0); }); }); + + testWidgets('keyboardDismissBehavior tests', (WidgetTester tester) async { + final List focusNodes = List.generate(50, (int i) => FocusNode()); + + Future boilerplate(ScrollViewKeyboardDismissBehavior behavior) { + return tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + padding: EdgeInsets.zero, + keyboardDismissBehavior: behavior, + child: Column( + children: focusNodes.map((FocusNode focusNode) { + return Container( + height: 50, + child: TextField(focusNode: focusNode), + ); + }).toList(), + ), + ), + ), + ), + ); + } + + // ScrollViewKeyboardDismissBehavior.onDrag dismiss keyboard on drag + await boilerplate(ScrollViewKeyboardDismissBehavior.onDrag); + + Finder finder = find.byType(TextField).first; + TextField textField = tester.widget(finder); + await tester.showKeyboard(finder); + expect(textField.focusNode!.hasFocus, isTrue); + + await tester.drag(finder, const Offset(0.0, -40.0)); + await tester.pumpAndSettle(); + expect(textField.focusNode!.hasFocus, isFalse); + + // ScrollViewKeyboardDismissBehavior.manual does no dismiss the keyboard + await boilerplate(ScrollViewKeyboardDismissBehavior.manual); + + finder = find.byType(TextField).first; + textField = tester.widget(finder); + await tester.showKeyboard(finder); + expect(textField.focusNode!.hasFocus, isTrue); + + await tester.drag(finder, const Offset(0.0, -40.0)); + await tester.pumpAndSettle(); + expect(textField.focusNode!.hasFocus, isTrue); + }); }