Fix date picker flickering (#167976)

## Description

This PR fixes a `DatePicker` flickering issue observed on some devices
with a small display.

### Before

When the date picker input dialog is shown the virtual keyboard opens
and reduces the available height. The current logic reacts by removing
the picker `TextField`. Because there is no more `TextField` the virtual
keyboard is hidden and the available height increases, at that moment
the `TextField` is shown again. Due to autofocus the virtual keyboard
opens which leads to the removal of the `TextField` and so on.


https://github.com/user-attachments/assets/deb70f2e-1c8b-47a9-9db6-47bc521e4eb7


### After

When there is not enough vertical space for the input date picker
dialog, the header is hidden instead of hiding the `TextField`.


https://github.com/user-attachments/assets/14051ee9-3b9a-4467-a4a0-4ee91b4979ea



## Related Issue

[Fixes DatePickerDialog text input causes keyboard to open and close on
a 480p Android
emulator](https://github.com/flutter/flutter/issues/140311).

## Tests

Adds 2 tests.
This commit is contained in:
Bruno Leroux 2025-04-29 17:03:09 +02:00 committed by GitHub
parent 04616f8dde
commit a46bf190ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 6 deletions

View File

@ -777,13 +777,21 @@ class _DatePickerDialogState extends State<DatePickerDialog> with RestorationMix
switch (orientation) {
case Orientation.portrait:
final bool isInputMode =
_entryMode.value == DatePickerEntryMode.inputOnly ||
_entryMode.value == DatePickerEntryMode.input;
// When the portrait dialog does not fit vertically, hide the header when the entry mode
// is input, or hide the picker when the entry mode is not input.
final bool showHeader = isFullyPortrait || !isInputMode;
final bool showPicker = isFullyPortrait || isInputMode;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
if (showHeader) header,
if (useMaterial3) Divider(height: 0, color: datePickerTheme.dividerColor),
if (isFullyPortrait) ...<Widget>[Expanded(child: picker), actions],
if (showPicker) ...<Widget>[Expanded(child: picker), actions],
],
);
case Orientation.landscape:
@ -3175,10 +3183,8 @@ class _InputDateRangePickerDialog extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
if (isFullyPortrait) ...<Widget>[Expanded(child: picker), actions],
],
// When the portrait dialog does not fit vertically, hide the header.
children: <Widget>[if (isFullyPortrait) header, Expanded(child: picker), actions],
);
},
);

View File

@ -1662,6 +1662,31 @@ void main() {
});
});
// Regression test for https://github.com/flutter/flutter/issues/140311.
testWidgets('Text field stays visible when orientation is portrait and height is reduced', (
WidgetTester tester,
) async {
addTearDown(tester.view.reset);
tester.view.physicalSize = const Size(720, 1280);
tester.view.devicePixelRatio = 1.0;
initialEntryMode = DatePickerEntryMode.input;
// Text field and header are visible by default.
await prepareDatePicker(tester, useMaterial3: true, (Future<DateTime?> range) async {
expect(find.byType(TextField), findsOneWidget);
expect(find.text('Select date'), findsOne);
});
// Simulate the portait mode on a device with a small display when the virtual
// keyboard is visible.
tester.view.viewInsets = const FakeViewPadding(bottom: 1000);
await tester.pumpAndSettle();
// Text field is visible and header is hidden.
expect(find.byType(TextField), findsOneWidget);
expect(find.text('Select date'), findsNothing);
});
// This is a regression test for https://github.com/flutter/flutter/issues/139120.
testWidgets('Dialog contents are visible - textScaler 0.88, 1.0, 2.0', (
WidgetTester tester,

View File

@ -1243,6 +1243,31 @@ void main() {
expect(tester.takeException(), null);
});
});
// Regression test for https://github.com/flutter/flutter/issues/140311.
testWidgets('Text field stays visible when orientation is portrait and height is reduced', (
WidgetTester tester,
) async {
addTearDown(tester.view.reset);
tester.view.physicalSize = const Size(720, 1280);
tester.view.devicePixelRatio = 1.0;
initialEntryMode = DatePickerEntryMode.input;
// Text fields and header are visible by default.
await preparePicker(tester, useMaterial3: true, (Future<DateTimeRange?> range) async {
expect(find.byType(TextField), findsNWidgets(2));
expect(find.text('Select range'), findsOne);
});
// Simulate the portait mode on a device with a small display when the virtual
// keyboard is visible.
tester.view.viewInsets = const FakeViewPadding(bottom: 1000);
await tester.pumpAndSettle();
// Text fields are visible and header is hidden
expect(find.byType(TextField), findsNWidgets(2));
expect(find.text('Select range'), findsNothing);
});
});
testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async {