Commit c1dfbd27 authored by Adam Barth's avatar Adam Barth

Not every month begins on a Monday (#4004)

Also, clean up the DatePicker to use more modern technology, such as a
grid for displaying the days of the month.

Fixes #3976
parent 8294b96f
...@@ -34,11 +34,12 @@ class DatePicker extends StatefulWidget { ...@@ -34,11 +34,12 @@ class DatePicker extends StatefulWidget {
/// Rather than creating a date picker directly, consider using /// Rather than creating a date picker directly, consider using
/// [showDatePicker] to show a date picker in a dialog. /// [showDatePicker] to show a date picker in a dialog.
DatePicker({ DatePicker({
Key key,
this.selectedDate, this.selectedDate,
this.onChanged, this.onChanged,
this.firstDate, this.firstDate,
this.lastDate this.lastDate
}) { }) : super(key: key) {
assert(selectedDate != null); assert(selectedDate != null);
assert(firstDate != null); assert(firstDate != null);
assert(lastDate != null); assert(lastDate != null);
...@@ -87,7 +88,7 @@ class _DatePickerState extends State<DatePicker> { ...@@ -87,7 +88,7 @@ class _DatePickerState extends State<DatePicker> {
config.onChanged(dateTime); config.onChanged(dateTime);
} }
static const double _calendarHeight = 210.0; static const double _calendarHeight = _kMaxDayPickerHeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -117,14 +118,14 @@ class _DatePickerState extends State<DatePicker> { ...@@ -117,14 +118,14 @@ class _DatePickerState extends State<DatePicker> {
break; break;
} }
return new Column( return new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
header, header,
new Container( new Container(
height: _calendarHeight, height: _calendarHeight,
child: picker child: picker
) )
], ]
crossAxisAlignment: CrossAxisAlignment.stretch
); );
} }
...@@ -132,7 +133,12 @@ class _DatePickerState extends State<DatePicker> { ...@@ -132,7 +133,12 @@ class _DatePickerState extends State<DatePicker> {
// 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 {
_DatePickerHeader({ this.selectedDate, this.mode, this.onModeChanged }) { _DatePickerHeader({
Key key,
this.selectedDate,
this.mode,
this.onModeChanged
}) : super(key: key) {
assert(selectedDate != null); assert(selectedDate != null);
assert(mode != null); assert(mode != null);
} }
...@@ -173,15 +179,15 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -173,15 +179,15 @@ class _DatePickerHeader extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_DatePickerMode.day), onTap: () => _handleChangeMode(_DatePickerMode.day),
child: new Text(new DateFormat("MMM").format(selectedDate).toUpperCase(), style: monthStyle) child: new Text(new DateFormat('MMM').format(selectedDate).toUpperCase(), style: monthStyle)
), ),
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_DatePickerMode.day), onTap: () => _handleChangeMode(_DatePickerMode.day),
child: new Text(new DateFormat("d").format(selectedDate), style: dayStyle) child: new Text(new DateFormat('d').format(selectedDate), style: dayStyle)
), ),
new GestureDetector( new GestureDetector(
onTap: () => _handleChangeMode(_DatePickerMode.year), onTap: () => _handleChangeMode(_DatePickerMode.year),
child: new Text(new DateFormat("yyyy").format(selectedDate), style: yearStyle) child: new Text(new DateFormat('yyyy').format(selectedDate), style: yearStyle)
) )
] ]
) )
...@@ -189,6 +195,27 @@ class _DatePickerHeader extends StatelessWidget { ...@@ -189,6 +195,27 @@ class _DatePickerHeader extends StatelessWidget {
} }
} }
const double _kDayPickerRowHeight = 30.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);
class _DayPickerGridDelegate extends GridDelegateWithInOrderChildPlacement {
@override
GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
assert(constraints.maxWidth < double.INFINITY);
final int columnCount = DateTime.DAYS_PER_WEEK;
return new GridSpecification.fromRegularTiles(
tileWidth: constraints.maxWidth / columnCount,
tileHeight: _kDayPickerRowHeight,
columnCount: columnCount,
rowCount: (childCount / columnCount).ceil()
);
}
}
final _DayPickerGridDelegate _kDayPickerGridDelegate = new _DayPickerGridDelegate();
/// Displays the days of a given month and allows choosing a day. /// Displays the days of a given month and allows choosing a day.
/// ///
/// The days are arranged in a rectangular grid with one column for each day of /// The days are arranged in a rectangular grid with one column for each day of
...@@ -205,11 +232,12 @@ class DayPicker extends StatelessWidget { ...@@ -205,11 +232,12 @@ class DayPicker extends StatelessWidget {
/// ///
/// Rarely used directly. Instead, typically used as part of a [DatePicker]. /// Rarely used directly. Instead, typically used as part of a [DatePicker].
DayPicker({ DayPicker({
Key key,
this.selectedDate, this.selectedDate,
this.currentDate, this.currentDate,
this.onChanged, this.onChanged,
this.displayedMonth this.displayedMonth
}) { }) : super(key: key) {
assert(selectedDate != null); assert(selectedDate != null);
assert(currentDate != null); assert(currentDate != null);
assert(onChanged != null); assert(onChanged != null);
...@@ -230,50 +258,36 @@ class DayPicker extends StatelessWidget { ...@@ -230,50 +258,36 @@ class DayPicker extends StatelessWidget {
/// The month whose days are displayed by this picker. /// The month whose days are displayed by this picker.
final DateTime displayedMonth; final DateTime displayedMonth;
List<Widget> _getDayHeaders(TextStyle headerStyle) {
final DateFormat dateFormat = new DateFormat();
final DateSymbols symbols = dateFormat.dateSymbols;
return symbols.NARROWWEEKDAYS.map((String weekDay) {
return new Center(child: new Text(weekDay, style: headerStyle));
}).toList(growable: false);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
TextStyle headerStyle = themeData.textTheme.caption.copyWith(fontWeight: FontWeight.w700); final TextStyle headerStyle = themeData.textTheme.caption.copyWith(fontWeight: FontWeight.w700);
TextStyle monthStyle = headerStyle.copyWith(fontSize: 14.0, height: 24.0 / 14.0); final TextStyle monthStyle = headerStyle.copyWith(fontSize: 14.0, height: 24.0 / 14.0);
TextStyle dayStyle = headerStyle.copyWith(fontWeight: FontWeight.w500); final TextStyle dayStyle = headerStyle.copyWith(fontWeight: FontWeight.w500);
DateFormat dateFormat = new DateFormat();
DateSymbols symbols = dateFormat.dateSymbols; final int year = displayedMonth.year;
final int month = displayedMonth.month;
List<Text> headers = <Text>[];
for (String weekDay in symbols.NARROWWEEKDAYS) {
headers.add(new Text(weekDay, style: headerStyle));
}
List<Widget> rows = <Widget>[
new Text(new DateFormat("MMMM y").format(displayedMonth), style: monthStyle),
new Flex(
children: headers,
mainAxisAlignment: MainAxisAlignment.spaceAround
)
];
int year = displayedMonth.year;
int month = displayedMonth.month;
// Dart's Date time constructor is very forgiving and will understand // Dart's Date time constructor is very forgiving and will understand
// month 13 as January of the next year. :) // month 13 as January of the next year. :)
int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays; final int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays;
int firstDay = new DateTime(year, month).day; // This assumes a start day of SUNDAY, but could be changed.
int weeksShown = 6; final int firstWeekday = new DateTime(year, month).weekday % 7;
List<int> days = <int>[ final List<Widget> labels = <Widget>[];
DateTime.SUNDAY, labels.addAll(_getDayHeaders(headerStyle));
DateTime.MONDAY, for (int i = 0; true; ++i) {
DateTime.TUESDAY, final int day = i - firstWeekday + 1;
DateTime.WEDNESDAY, if (day > daysInMonth)
DateTime.THURSDAY, break;
DateTime.FRIDAY, if (day < 1) {
DateTime.SATURDAY labels.add(new Container());
];
int daySlots = weeksShown * days.length;
List<Widget> labels = <Widget>[];
for (int i = 0; i < daySlots; i++) {
// This assumes a start day of SUNDAY, but could be changed.
int day = i - firstDay + 1;
Widget item;
if (day < 1 || day > daysInMonth) {
item = new Text("");
} else { } else {
BoxDecoration decoration; BoxDecoration decoration;
TextStyle itemStyle = dayStyle; TextStyle itemStyle = dayStyle;
...@@ -293,31 +307,32 @@ class DayPicker extends StatelessWidget { ...@@ -293,31 +307,32 @@ class DayPicker extends StatelessWidget {
itemStyle = itemStyle.copyWith(color: themeData.accentColor); itemStyle = itemStyle.copyWith(color: themeData.accentColor);
} }
item = new GestureDetector( labels.add(new GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
DateTime result = new DateTime(year, month, day); DateTime result = new DateTime(year, month, day);
onChanged(result); onChanged(result);
}, },
child: new Container( child: new Container(
height: 30.0,
decoration: decoration, decoration: decoration,
child: new Center( child: new Center(
child: new Text(day.toString(), style: itemStyle) child: new Text(day.toString(), style: itemStyle)
) )
) )
); ));
} }
labels.add(new Flexible(child: item));
}
for (int w = 0; w < weeksShown; w++) {
int startIndex = w * days.length;
rows.add(new Row(
children: labels.sublist(startIndex, startIndex + days.length)
));
} }
return new Column(children: rows); return new Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
new Text(new DateFormat('MMMM y').format(displayedMonth), style: monthStyle),
new CustomGrid(
delegate: _kDayPickerGridDelegate,
children: labels
)
]
);
} }
} }
...@@ -399,22 +414,18 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -399,22 +414,18 @@ class _MonthPickerState extends State<MonthPicker> {
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month; return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
} }
List<Widget> buildItems(BuildContext context, int start, int count) { List<Widget> _buildItems(BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>(); final List<Widget> result = new List<Widget>();
DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12); final DateTime startDate = new DateTime(config.firstDate.year + start ~/ 12, config.firstDate.month + start % 12);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12); DateTime displayedMonth = new DateTime(startDate.year + i ~/ 12, startDate.month + i % 12);
Widget item = new Container( result.add(new DayPicker(
height: config.itemExtent,
key: new ObjectKey(displayedMonth), key: new ObjectKey(displayedMonth),
child: new DayPicker( selectedDate: config.selectedDate,
selectedDate: config.selectedDate, currentDate: _currentDate,
currentDate: _currentDate, onChanged: config.onChanged,
onChanged: config.onChanged, displayedMonth: displayedMonth
displayedMonth: displayedMonth ));
)
);
result.add(item);
} }
return result; return result;
} }
...@@ -426,7 +437,7 @@ class _MonthPickerState extends State<MonthPicker> { ...@@ -426,7 +437,7 @@ class _MonthPickerState extends State<MonthPicker> {
initialScrollOffset: config.itemExtent * _monthDelta(config.firstDate, config.selectedDate), initialScrollOffset: config.itemExtent * _monthDelta(config.firstDate, config.selectedDate),
itemExtent: config.itemExtent, itemExtent: config.itemExtent,
itemCount: _monthDelta(config.firstDate, config.lastDate) + 1, itemCount: _monthDelta(config.firstDate, config.lastDate) + 1,
itemBuilder: buildItems itemBuilder: _buildItems
); );
} }
...@@ -488,7 +499,7 @@ class YearPicker extends StatefulWidget { ...@@ -488,7 +499,7 @@ class YearPicker extends StatefulWidget {
class _YearPickerState extends State<YearPicker> { class _YearPickerState extends State<YearPicker> {
static const double _itemExtent = 50.0; static const double _itemExtent = 50.0;
List<Widget> buildItems(BuildContext context, int start, int count) { List<Widget> _buildItems(BuildContext context, int start, int count) {
TextStyle style = Theme.of(context).textTheme.body1.copyWith(color: Colors.black54); TextStyle style = Theme.of(context).textTheme.body1.copyWith(color: Colors.black54);
List<Widget> items = new List<Widget>(); List<Widget> items = new List<Widget>();
for (int i = start; i < start + count; i++) { for (int i = start; i < start + count; i++) {
...@@ -522,7 +533,7 @@ class _YearPickerState extends State<YearPicker> { ...@@ -522,7 +533,7 @@ class _YearPickerState extends State<YearPicker> {
return new ScrollableLazyList( return new ScrollableLazyList(
itemExtent: _itemExtent, itemExtent: _itemExtent,
itemCount: config.lastDate.year - config.firstDate.year + 1, itemCount: config.lastDate.year - config.firstDate.year + 1,
itemBuilder: buildItems itemBuilder: _buildItems
); );
} }
} }
...@@ -1582,7 +1582,7 @@ class CustomGrid extends GridRenderObjectWidgetBase { ...@@ -1582,7 +1582,7 @@ class CustomGrid extends GridRenderObjectWidgetBase {
/// Uses a grid layout with a fixed column count. /// Uses a grid layout with a fixed column count.
/// ///
/// For details about the grid layout algorithm, see [MaxTileWidthGridDelegate]. /// For details about the grid layout algorithm, see [FixedColumnCountGridDelegate].
class FixedColumnCountGrid extends GridRenderObjectWidgetBase { class FixedColumnCountGrid extends GridRenderObjectWidgetBase {
FixedColumnCountGrid({ FixedColumnCountGrid({
Key key, Key key,
......
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