Unverified Commit 2d2edbf7 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Date picker layout exceptions (#31514)

Fixed several layout issues with the material date picker. Mostly just removed hard coded sizes to allow the grid view to scroll instead of overflowing.
parent 61236c87
...@@ -45,21 +45,12 @@ enum DatePickerMode { ...@@ -45,21 +45,12 @@ enum DatePickerMode {
year, year,
} }
const double _kDatePickerHeaderPortraitHeight = 100.0;
const double _kDatePickerHeaderLandscapeWidth = 168.0;
const Duration _kMonthScrollDuration = Duration(milliseconds: 200); const Duration _kMonthScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0; const double _kDayPickerRowHeight = 42.0;
const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday. 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. // Two extra rows: one for the day-of-week header and one for the month header.
const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2); 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 // Shows the selected date in large font and toggles between year and day mode
class _DatePickerHeader extends StatelessWidget { class _DatePickerHeader extends StatelessWidget {
const _DatePickerHeader({ const _DatePickerHeader({
...@@ -100,8 +91,8 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -100,8 +91,8 @@ class _DatePickerHeader extends StatelessWidget {
yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70; yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70;
break; break;
} }
final TextStyle dayStyle = headerTextTheme.display1.copyWith(color: dayColor, height: 1.4); final TextStyle dayStyle = headerTextTheme.display1.copyWith(color: dayColor);
final TextStyle yearStyle = headerTextTheme.subhead.copyWith(color: yearColor, height: 1.4); final TextStyle yearStyle = headerTextTheme.subhead.copyWith(color: yearColor);
Color backgroundColor; Color backgroundColor;
switch (themeData.brightness) { switch (themeData.brightness) {
...@@ -113,18 +104,14 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -113,18 +104,14 @@ class _DatePickerHeader extends StatelessWidget {
break; break;
} }
double width;
double height;
EdgeInsets padding; EdgeInsets padding;
MainAxisAlignment mainAxisAlignment; MainAxisAlignment mainAxisAlignment;
switch (orientation) { switch (orientation) {
case Orientation.portrait: case Orientation.portrait:
height = _kDatePickerHeaderPortraitHeight; padding = const EdgeInsets.all(16.0);
padding = const EdgeInsets.symmetric(horizontal: 16.0);
mainAxisAlignment = MainAxisAlignment.center; mainAxisAlignment = MainAxisAlignment.center;
break; break;
case Orientation.landscape: case Orientation.landscape:
width = _kDatePickerHeaderLandscapeWidth;
padding = const EdgeInsets.all(8.0); padding = const EdgeInsets.all(8.0);
mainAxisAlignment = MainAxisAlignment.start; mainAxisAlignment = MainAxisAlignment.start;
break; break;
...@@ -157,8 +144,6 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -157,8 +144,6 @@ class _DatePickerHeader extends StatelessWidget {
); );
return Container( return Container(
width: width,
height: height,
padding: padding, padding: padding,
color: backgroundColor, color: backgroundColor,
child: Column( child: Column(
...@@ -210,7 +195,8 @@ class _DayPickerGridDelegate extends SliverGridDelegate { ...@@ -210,7 +195,8 @@ class _DayPickerGridDelegate extends SliverGridDelegate {
SliverGridLayout getLayout(SliverConstraints constraints) { SliverGridLayout getLayout(SliverConstraints constraints) {
const int columnCount = DateTime.daysPerWeek; const int columnCount = DateTime.daysPerWeek;
final double tileWidth = constraints.crossAxisExtent / columnCount; 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( return SliverGridRegularTileLayout(
crossAxisCount: columnCount, crossAxisCount: columnCount,
mainAxisStride: tileHeight, mainAxisStride: tileHeight,
...@@ -493,6 +479,7 @@ class DayPicker extends StatelessWidget { ...@@ -493,6 +479,7 @@ class DayPicker extends StatelessWidget {
child: GridView.custom( child: GridView.custom(
gridDelegate: _kDayPickerGridDelegate, gridDelegate: _kDayPickerGridDelegate,
childrenDelegate: SliverChildListDelegate(labels, addRepaintBoundaries: false), childrenDelegate: SliverChildListDelegate(labels, addRepaintBoundaries: false),
padding: EdgeInsets.zero,
), ),
), ),
], ],
...@@ -682,7 +669,8 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat ...@@ -682,7 +669,8 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( 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, height: _kMaxDayPickerHeight,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
...@@ -994,12 +982,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -994,12 +982,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final Widget picker = Flexible( final Widget picker = _buildPicker();
child: SizedBox(
height: _kMaxDayPickerHeight,
child: _buildPicker(),
),
);
final Widget actions = ButtonTheme.bar( final Widget actions = ButtonTheme.bar(
child: ButtonBar( child: ButtonBar(
children: <Widget>[ children: <Widget>[
...@@ -1014,6 +997,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -1014,6 +997,7 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
], ],
), ),
); );
final Dialog dialog = Dialog( final Dialog dialog = Dialog(
child: OrientationBuilder( child: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) { builder: (BuildContext context, Orientation orientation) {
...@@ -1026,44 +1010,35 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { ...@@ -1026,44 +1010,35 @@ class _DatePickerDialogState extends State<_DatePickerDialog> {
); );
switch (orientation) { switch (orientation) {
case Orientation.portrait: case Orientation.portrait:
return SizedBox( return Container(
width: _kMonthPickerPortraitWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
Container(
color: theme.dialogBackgroundColor, color: theme.dialogBackgroundColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
picker, header,
Flexible(child: picker),
actions, actions,
], ],
), ),
),
],
),
); );
case Orientation.landscape: case Orientation.landscape:
return SizedBox( return Container(
height: _kDatePickerLandscapeHeight, color: theme.dialogBackgroundColor,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
header, Flexible(child: header),
Flexible( Flexible(
child: Container( flex: 2, // have the picker take up 2/3 of the dialog width
width: _kMonthPickerLandscapeWidth,
color: theme.dialogBackgroundColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[picker, actions], children: <Widget>[
), Flexible(child: picker),
actions
],
), ),
), ),
], ],
......
...@@ -773,4 +773,88 @@ void _tests() { ...@@ -773,4 +773,88 @@ void _tests() {
// button and the right edge of the 800 wide window. // button and the right edge of the 800 wide window.
expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight); 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<void> _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);
});
});
} }
...@@ -223,6 +223,67 @@ void main() { ...@@ -223,6 +223,67 @@ void main() {
await tester.tap(find.text('ANNULER')); 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<void> _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<void> _pumpBoilerplate( Future<void> _pumpBoilerplate(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment