diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index fbc5c78fc21..c13f952a93e 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -172,7 +172,8 @@ class DayPicker extends StatelessWidget { @required this.onChanged, @required this.firstDate, @required this.lastDate, - @required this.displayedMonth + @required this.displayedMonth, + this.selectableDayPredicate }) : super(key: key) { assert(selectedDate != null); assert(currentDate != null); @@ -202,6 +203,9 @@ class DayPicker extends StatelessWidget { /// The month whose days are displayed by this picker. final DateTime displayedMonth; + /// Optional user supplied predicate function to customize selectable days. + final SelectableDayPredicate selectableDayPredicate; + List _getDayHeaders(TextStyle headerStyle) { final DateFormat dateFormat = new DateFormat(); final DateSymbols symbols = dateFormat.dateSymbols; @@ -230,7 +234,9 @@ class DayPicker extends StatelessWidget { labels.add(new Container()); } else { final DateTime dayToBuild = new DateTime(year, month, day); - final bool disabled = dayToBuild.isAfter(lastDate) || dayToBuild.isBefore(firstDate); + final bool disabled = dayToBuild.isAfter(lastDate) + || dayToBuild.isBefore(firstDate) + || (selectableDayPredicate != null && !selectableDayPredicate(dayToBuild)); BoxDecoration decoration; TextStyle itemStyle = themeData.textTheme.body1; @@ -315,7 +321,8 @@ class MonthPicker extends StatefulWidget { @required this.selectedDate, @required this.onChanged, @required this.firstDate, - @required this.lastDate + @required this.lastDate, + this.selectableDayPredicate }) : super(key: key) { assert(selectedDate != null); assert(onChanged != null); @@ -337,6 +344,9 @@ class MonthPicker extends StatefulWidget { /// The latest date the user is permitted to pick. final DateTime lastDate; + /// Optional user supplied predicate function to customize selectable days. + final SelectableDayPredicate selectableDayPredicate; + @override _MonthPickerState createState() => new _MonthPickerState(); } @@ -399,7 +409,8 @@ class _MonthPickerState extends State { onChanged: config.onChanged, firstDate: config.firstDate, lastDate: config.lastDate, - displayedMonth: monthToBuild + displayedMonth: monthToBuild, + selectableDayPredicate: config.selectableDayPredicate )); } return result; @@ -570,12 +581,14 @@ class _DatePickerDialog extends StatefulWidget { Key key, this.initialDate, this.firstDate, - this.lastDate + this.lastDate, + this.selectableDayPredicate }) : super(key: key); final DateTime initialDate; final DateTime firstDate; final DateTime lastDate; + final SelectableDayPredicate selectableDayPredicate; @override _DatePickerDialogState createState() => new _DatePickerDialogState(); @@ -631,7 +644,8 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { selectedDate: _selectedDate, onChanged: _handleDayChanged, firstDate: config.firstDate, - lastDate: config.lastDate + lastDate: config.lastDate, + selectableDayPredicate: config.selectableDayPredicate ); case _DatePickerMode.year: return new YearPicker( @@ -717,11 +731,20 @@ class _DatePickerDialogState extends State<_DatePickerDialog> { } } +/// Signature for predicating dates for enabled date selections. +/// +/// See [showDatePicker]. +typedef bool SelectableDayPredicate(DateTime day); + /// Shows a dialog containing a material design date picker. /// /// The returned [Future] resolves to the date selected by the user when the /// user closes the dialog. If the user cancels the dialog, null is returned. /// +/// An optional [selectableDayPredicate] function can be passed in to customize +/// the days to enable for selection. If provided, only the days that +/// [selectableDayPredicate] returned true for will be selectable. +/// /// See also: /// /// * [showTimePicker] @@ -730,14 +753,23 @@ Future showDatePicker({ @required BuildContext context, @required DateTime initialDate, @required DateTime firstDate, - @required DateTime lastDate + @required DateTime lastDate, + SelectableDayPredicate selectableDayPredicate }) async { + assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate'); + assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate'); + assert(!firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate'); + assert( + selectableDayPredicate == null || selectableDayPredicate(initialDate), + 'Provided initialDate must satisfy provided selectableDayPredicate' + ); return await showDialog( context: context, child: new _DatePickerDialog( initialDate: initialDate, firstDate: firstDate, - lastDate: lastDate + lastDate: lastDate, + selectableDayPredicate: selectableDayPredicate ) ); } diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index f5e632df9b5..133c06c39af 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -10,6 +10,7 @@ void main() { DateTime firstDate; DateTime lastDate; DateTime initialDate; + SelectableDayPredicate selectableDayPredicate; setUp(() { firstDate = new DateTime(2001, DateTime.JANUARY, 1); @@ -144,6 +145,7 @@ void main() { initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, + selectableDayPredicate: selectableDayPredicate ); await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1)); @@ -257,4 +259,18 @@ void main() { expect(await date, equals(new DateTime(2016, DateTime.DECEMBER, 10))); }); }); + + testWidgets('Only predicate days are selectable', (WidgetTester tester) async { + initialDate = new DateTime(2017, DateTime.JANUARY, 16); + firstDate = new DateTime(2017, DateTime.JANUARY, 10); + lastDate = new DateTime(2017, DateTime.JANUARY, 20); + selectableDayPredicate = (DateTime day) => day.day.isEven; + await preparePicker(tester, (Future date) async { + await tester.tap(find.text('10')); // Even, works. + await tester.tap(find.text('13')); // Odd, doesn't work. + await tester.tap(find.text('17')); // Odd, doesn't work. + await tester.tap(find.text('OK')); + expect(await date, equals(new DateTime(2017, DateTime.JANUARY, 10))); + }); + }); }