diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index d038f8e589..5a120e934e 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -45,21 +45,12 @@ enum DatePickerMode { year, } -const double _kDatePickerHeaderPortraitHeight = 100.0; -const double _kDatePickerHeaderLandscapeWidth = 168.0; - const Duration _kMonthScrollDuration = Duration(milliseconds: 200); const double _kDayPickerRowHeight = 42.0; const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday. // Two extra rows: one for the day-of-week header and one for the month header. const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2); -const double _kMonthPickerPortraitWidth = 330.0; -const double _kMonthPickerLandscapeWidth = 344.0; - -const double _kDialogActionBarHeight = 52.0; -const double _kDatePickerLandscapeHeight = _kMaxDayPickerHeight + _kDialogActionBarHeight; - // Shows the selected date in large font and toggles between year and day mode class _DatePickerHeader extends StatelessWidget { const _DatePickerHeader({ @@ -100,8 +91,8 @@ class _DatePickerHeader extends StatelessWidget { yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70; break; } - final TextStyle dayStyle = headerTextTheme.display1.copyWith(color: dayColor, height: 1.4); - final TextStyle yearStyle = headerTextTheme.subhead.copyWith(color: yearColor, height: 1.4); + final TextStyle dayStyle = headerTextTheme.display1.copyWith(color: dayColor); + final TextStyle yearStyle = headerTextTheme.subhead.copyWith(color: yearColor); Color backgroundColor; switch (themeData.brightness) { @@ -113,18 +104,14 @@ class _DatePickerHeader extends StatelessWidget { break; } - double width; - double height; EdgeInsets padding; MainAxisAlignment mainAxisAlignment; switch (orientation) { case Orientation.portrait: - height = _kDatePickerHeaderPortraitHeight; - padding = const EdgeInsets.symmetric(horizontal: 16.0); + padding = const EdgeInsets.all(16.0); mainAxisAlignment = MainAxisAlignment.center; break; case Orientation.landscape: - width = _kDatePickerHeaderLandscapeWidth; padding = const EdgeInsets.all(8.0); mainAxisAlignment = MainAxisAlignment.start; break; @@ -157,8 +144,6 @@ class _DatePickerHeader extends StatelessWidget { ); return Container( - width: width, - height: height, padding: padding, color: backgroundColor, child: Column( @@ -210,7 +195,8 @@ class _DayPickerGridDelegate extends SliverGridDelegate { SliverGridLayout getLayout(SliverConstraints constraints) { const int columnCount = DateTime.daysPerWeek; final double tileWidth = constraints.crossAxisExtent / columnCount; - final double tileHeight = math.min(_kDayPickerRowHeight, constraints.viewportMainAxisExtent / (_kMaxDayPickerRowCount + 1)); + final double viewTileHeight = constraints.viewportMainAxisExtent / (_kMaxDayPickerRowCount + 1); + final double tileHeight = math.max(_kDayPickerRowHeight, viewTileHeight); return SliverGridRegularTileLayout( crossAxisCount: columnCount, mainAxisStride: tileHeight, @@ -493,6 +479,7 @@ class DayPicker extends StatelessWidget { child: GridView.custom( gridDelegate: _kDayPickerGridDelegate, childrenDelegate: SliverChildListDelegate(labels, addRepaintBoundaries: false), + padding: EdgeInsets.zero, ), ), ], @@ -682,7 +669,8 @@ class _MonthPickerState extends State with SingleTickerProviderStat @override Widget build(BuildContext context) { return SizedBox( - width: _kMonthPickerPortraitWidth, + // The month picker just adds month navigation to the day picker, so make + // it the same height as the DayPicker height: _kMaxDayPickerHeight, child: Stack( children: [ @@ -994,12 +982,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - final Widget picker = Flexible( - child: SizedBox( - height: _kMaxDayPickerHeight, - child: _buildPicker(), - ), - ); + final Widget picker = _buildPicker(); final Widget actions = ButtonTheme.bar( child: ButtonBar( children: [ @@ -1014,6 +997,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ], ), ); + final Dialog dialog = Dialog( child: OrientationBuilder( builder: (BuildContext context, Orientation orientation) { @@ -1026,44 +1010,35 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ); switch (orientation) { case Orientation.portrait: - return SizedBox( - width: _kMonthPickerPortraitWidth, + return Container( + color: theme.dialogBackgroundColor, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ header, - Container( - color: theme.dialogBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - picker, - actions, - ], - ), - ), + Flexible(child: picker), + actions, ], ), ); case Orientation.landscape: - return SizedBox( - height: _kDatePickerLandscapeHeight, + return Container( + color: theme.dialogBackgroundColor, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - header, + Flexible(child: header), Flexible( - child: Container( - width: _kMonthPickerLandscapeWidth, - color: theme.dialogBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [picker, actions], - ), + flex: 2, // have the picker take up 2/3 of the dialog width + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible(child: picker), + actions + ], ), ), ], diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index 3fe824eab5..0bb78eefe6 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -773,4 +773,88 @@ void _tests() { // button and the right edge of the 800 wide window. expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight); }); + + group('screen configurations', () { + // Test various combinations of screen sizes, orientations and text scales + // to ensure the layout doesn't overflow and cause an exception to be thrown. + + // Regression tests for https://github.com/flutter/flutter/issues/21383 + // Regression tests for https://github.com/flutter/flutter/issues/19744 + // Regression tests for https://github.com/flutter/flutter/issues/17745 + + // Common screen size roughly based on a Pixel 1 + const Size kCommonScreenSizePortrait = Size(1070, 1770); + const Size kCommonScreenSizeLandscape = Size(1770, 1070); + + // Small screen size based on a LG K130 + const Size kSmallScreenSizePortrait = Size(320, 521); + const Size kSmallScreenSizeLandscape = Size(521, 320); + + Future _showPicker(WidgetTester tester, Size size, [double textScaleFactor = 1.0]) async { + tester.binding.window.physicalSizeTestValue = size; + tester.binding.window.devicePixelRatioTestValue = 1.0; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return RaisedButton( + child: const Text('X'), + onPressed: () { + showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + ); + }, + ); + }, + ), + ), + ); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + } + + testWidgets('common screen size - portrait', (WidgetTester tester) async { + await _showPicker(tester, kCommonScreenSizePortrait); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - landscape', (WidgetTester tester) async { + await _showPicker(tester, kCommonScreenSizeLandscape); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - portrait - textScale 1.3', (WidgetTester tester) async { + await _showPicker(tester, kCommonScreenSizePortrait, 1.3); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - landscape - textScale 1.3', (WidgetTester tester) async { + await _showPicker(tester, kCommonScreenSizeLandscape, 1.3); + expect(tester.takeException(), isNull); + }); + + testWidgets('small screen size - portrait', (WidgetTester tester) async { + await _showPicker(tester, kSmallScreenSizePortrait); + expect(tester.takeException(), isNull); + }); + + testWidgets('small screen size - landscape', (WidgetTester tester) async { + await _showPicker(tester, kSmallScreenSizeLandscape); + expect(tester.takeException(), isNull); + }); + + testWidgets('small screen size - portrait -textScale 1.3', (WidgetTester tester) async { + await _showPicker(tester, kSmallScreenSizePortrait, 1.3); + expect(tester.takeException(), isNull); + }); + + testWidgets('small screen size - landscape - textScale 1.3', (WidgetTester tester) async { + await _showPicker(tester, kSmallScreenSizeLandscape, 1.3); + expect(tester.takeException(), isNull); + }); + }); + } diff --git a/packages/flutter_localizations/test/date_picker_test.dart b/packages/flutter_localizations/test/date_picker_test.dart index 5926462252..ca944d6421 100644 --- a/packages/flutter_localizations/test/date_picker_test.dart +++ b/packages/flutter_localizations/test/date_picker_test.dart @@ -223,6 +223,67 @@ void main() { await tester.tap(find.text('ANNULER')); }); + + group('locale fonts don\'t overflow layout', () { + // Test screen layouts in various locales to ensure the fonts used + // don't overflow the layout + + // Common screen size roughly based on a Pixel 1 + const Size kCommonScreenSizePortrait = Size(1070, 1770); + const Size kCommonScreenSizeLandscape = Size(1770, 1070); + + Future _showPicker(WidgetTester tester, Locale locale, Size size) async { + tester.binding.window.physicalSizeTestValue = size; + tester.binding.window.devicePixelRatioTestValue = 1.0; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Localizations( + locale: locale, + delegates: GlobalMaterialLocalizations.delegates, + child: RaisedButton( + child: const Text('X'), + onPressed: () { + showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: lastDate, + ); + }, + ), + ); + }, + ), + ) + ); + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + } + + // Regression test for https://github.com/flutter/flutter/issues/20171 + testWidgets('common screen size - portrait - Chinese', (WidgetTester tester) async { + await _showPicker(tester, const Locale('zh', 'CN'), kCommonScreenSizePortrait); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - landscape - Chinese', (WidgetTester tester) async { + await _showPicker(tester, const Locale('zh', 'CN'), kCommonScreenSizeLandscape); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - portrait - Japanese', (WidgetTester tester) async { + await _showPicker(tester, const Locale('ja', 'JA'), kCommonScreenSizePortrait); + expect(tester.takeException(), isNull); + }); + + testWidgets('common screen size - landscape - Japanese', (WidgetTester tester) async { + await _showPicker(tester, const Locale('ja', 'JA'), kCommonScreenSizeLandscape); + expect(tester.takeException(), isNull); + }); + }); + } Future _pumpBoilerplate(