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.
This commit is contained in:
@@ -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<MonthPicker> 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: <Widget>[
|
||||
@@ -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: <Widget>[
|
||||
@@ -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: <Widget>[
|
||||
header,
|
||||
Container(
|
||||
color: theme.dialogBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
header,
|
||||
Flexible(child: header),
|
||||
Flexible(
|
||||
child: Container(
|
||||
width: _kMonthPickerLandscapeWidth,
|
||||
color: theme.dialogBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[picker, actions],
|
||||
),
|
||||
flex: 2, // have the picker take up 2/3 of the dialog width
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Flexible(child: picker),
|
||||
actions
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<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() {
|
||||
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user