date_picker.dart 24.5 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:math' as math;
7

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/rendering.dart';
10 11
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
Hixie's avatar
Hixie committed
12 13
import 'package:intl/date_symbols.dart';
import 'package:intl/intl.dart';
14

15 16
import 'button_bar.dart';
import 'button.dart';
17
import 'colors.dart';
18
import 'debug.dart';
19 20
import 'dialog.dart';
import 'flat_button.dart';
21
import 'icon_button.dart';
22
import 'icon.dart';
Ian Hickson's avatar
Ian Hickson committed
23
import 'icons.dart';
24 25
import 'ink_well.dart';
import 'theme.dart';
26
import 'typography.dart';
27

28
enum _DatePickerMode { day, year }
29

30 31
const double _kDatePickerHeaderPortraitHeight = 100.0;
const double _kDatePickerHeaderLandscapeWidth = 168.0;
32

33 34 35 36 37
const Duration _kMonthScrollDuration = const 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);
38 39 40 41 42 43

const double _kMonthPickerPortraitWidth = 330.0;
const double _kMonthPickerLandscapeWidth = 344.0;

const double _kDialogActionBarHeight = 52.0;
const double _kDatePickerLandscapeHeight = _kMaxDayPickerHeight + _kDialogActionBarHeight;
44 45

// Shows the selected date in large font and toggles between year and day mode
46
class _DatePickerHeader extends StatelessWidget {
47 48
  _DatePickerHeader({
    Key key,
49 50
    @required this.selectedDate,
    @required this.mode,
51 52
    @required this.onModeChanged,
    @required this.orientation,
53
  }) : super(key: key) {
54 55
    assert(selectedDate != null);
    assert(mode != null);
56
    assert(orientation != null);
57 58
  }

59 60 61
  final DateTime selectedDate;
  final _DatePickerMode mode;
  final ValueChanged<_DatePickerMode> onModeChanged;
62
  final Orientation orientation;
63

64
  void _handleChangeMode(_DatePickerMode value) {
Adam Barth's avatar
Adam Barth committed
65 66
    if (value != mode)
      onModeChanged(value);
67 68
  }

69
  @override
70
  Widget build(BuildContext context) {
71 72
    ThemeData themeData = Theme.of(context);
    TextTheme headerTextTheme = themeData.primaryTextTheme;
73 74
    Color dayColor;
    Color yearColor;
75
    switch(themeData.primaryColorBrightness) {
76
      case Brightness.light:
77 78
        dayColor = mode == _DatePickerMode.day ? Colors.black87 : Colors.black54;
        yearColor = mode == _DatePickerMode.year ? Colors.black87 : Colors.black54;
79
        break;
80
      case Brightness.dark:
81 82
        dayColor = mode == _DatePickerMode.day ? Colors.white : Colors.white70;
        yearColor = mode == _DatePickerMode.year ? Colors.white : Colors.white70;
83 84
        break;
    }
85 86 87 88 89
    TextStyle dayStyle = headerTextTheme.display1.copyWith(color: dayColor, height: 1.4);
    TextStyle yearStyle = headerTextTheme.subhead.copyWith(color: yearColor, height: 1.4);

    Color backgroundColor;
    switch (themeData.brightness) {
90
      case Brightness.light:
91 92
        backgroundColor = themeData.primaryColor;
        break;
93
      case Brightness.dark:
94 95 96
        backgroundColor = themeData.backgroundColor;
        break;
    }
97

98
    double width;
99
    double height;
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
    EdgeInsets padding;
    MainAxisAlignment mainAxisAlignment;
    switch (orientation) {
      case Orientation.portrait:
        height = _kDatePickerHeaderPortraitHeight;
        padding = const EdgeInsets.symmetric(horizontal: 24.0);
        mainAxisAlignment = MainAxisAlignment.center;
        break;
      case Orientation.landscape:
        width = _kDatePickerHeaderLandscapeWidth;
        padding = const EdgeInsets.all(16.0);
        mainAxisAlignment = MainAxisAlignment.start;
        break;
    }

115
    return new Container(
116 117 118
      width: width,
      height: height,
      padding: padding,
119
      decoration: new BoxDecoration(backgroundColor: backgroundColor),
120
      child: new Column(
121
        mainAxisAlignment: mainAxisAlignment,
122
        crossAxisAlignment: CrossAxisAlignment.start,
123 124
        children: <Widget>[
          new GestureDetector(
125
            onTap: () => _handleChangeMode(_DatePickerMode.year),
126
            child: new Text(new DateFormat('yyyy').format(selectedDate), style: yearStyle),
127 128 129
          ),
          new GestureDetector(
            onTap: () => _handleChangeMode(_DatePickerMode.day),
130
            child: new Text(new DateFormat('E, MMM\u00a0d').format(selectedDate), style: dayStyle),
131
          ),
132 133
        ],
      ),
134 135 136 137
    );
  }
}

138 139 140
class _DayPickerGridDelegate extends SliverGridDelegate {
  const _DayPickerGridDelegate();

141
  @override
142
  SliverGridLayout getLayout(SliverConstraints constraints) {
143
    final int columnCount = DateTime.DAYS_PER_WEEK;
144 145 146 147 148 149 150 151
    final double tileWidth = constraints.crossAxisExtent / columnCount;
    final double tileHeight = math.min(_kDayPickerRowHeight, constraints.viewportMainAxisExtent / (_kMaxDayPickerRowCount + 1));
    return new SliverGridRegularTileLayout(
      crossAxisCount: columnCount,
      mainAxisStride: tileHeight,
      crossAxisStride: tileWidth,
      childMainAxisExtent: tileHeight,
      childCrossAxisExtent: tileWidth,
152 153
    );
  }
154 155 156

  @override
  bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false;
157 158
}

159
const _DayPickerGridDelegate _kDayPickerGridDelegate = const _DayPickerGridDelegate();
160

161 162 163 164 165
/// 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 week.
///
166 167
/// The day picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
168 169
///
/// See also:
170
///
171
///  * [showDatePicker].
172
///  * <https://material.google.com/components/pickers.html#pickers-date-pickers>
173
class DayPicker extends StatelessWidget {
174 175 176
  /// Creates a day picker.
  ///
  /// Rarely used directly. Instead, typically used as part of a [DatePicker].
177
  DayPicker({
178
    Key key,
179 180 181
    @required this.selectedDate,
    @required this.currentDate,
    @required this.onChanged,
182 183
    @required this.firstDate,
    @required this.lastDate,
184
    @required this.displayedMonth,
185
    this.selectableDayPredicate,
186
  }) : super(key: key) {
187 188
    assert(selectedDate != null);
    assert(currentDate != null);
189
    assert(onChanged != null);
190
    assert(displayedMonth != null);
191 192
    assert(!firstDate.isAfter(lastDate));
    assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate));
193 194
  }

195 196 197
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
198
  final DateTime selectedDate;
199 200

  /// The current date at the time the picker is displayed.
201
  final DateTime currentDate;
202 203

  /// Called when the user picks a day.
Hixie's avatar
Hixie committed
204
  final ValueChanged<DateTime> onChanged;
205

206 207 208 209 210 211
  /// The earliest date the user is permitted to pick.
  final DateTime firstDate;

  /// The latest date the user is permitted to pick.
  final DateTime lastDate;

212
  /// The month whose days are displayed by this picker.
213 214
  final DateTime displayedMonth;

215 216 217
  /// Optional user supplied predicate function to customize selectable days.
  final SelectableDayPredicate selectableDayPredicate;

218 219 220 221 222 223 224 225
  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);
  }

226
  @override
227
  Widget build(BuildContext context) {
228 229 230
    final ThemeData themeData = Theme.of(context);
    final int year = displayedMonth.year;
    final int month = displayedMonth.month;
231 232
    // Dart's Date time constructor is very forgiving and will understand
    // month 13 as January of the next year. :)
233 234 235 236
    final int daysInMonth = new DateTime(year, month + 1).difference(new DateTime(year, month)).inDays;
    // This assumes a start day of SUNDAY, but could be changed.
    final int firstWeekday = new DateTime(year, month).weekday % 7;
    final List<Widget> labels = <Widget>[];
237
    labels.addAll(_getDayHeaders(themeData.textTheme.caption));
238 239 240 241 242 243
    for (int i = 0; true; ++i) {
      final int day = i - firstWeekday + 1;
      if (day > daysInMonth)
        break;
      if (day < 1) {
        labels.add(new Container());
244
      } else {
245
        final DateTime dayToBuild = new DateTime(year, month, day);
246 247 248
        final bool disabled = dayToBuild.isAfter(lastDate)
            || dayToBuild.isBefore(firstDate)
            || (selectableDayPredicate != null && !selectableDayPredicate(dayToBuild));
249

Ian Hickson's avatar
Ian Hickson committed
250
        BoxDecoration decoration;
251
        TextStyle itemStyle = themeData.textTheme.body1;
Hans Muller's avatar
Hans Muller committed
252 253 254

        if (selectedDate.year == year && selectedDate.month == month && selectedDate.day == day) {
          // The selected day gets a circle background highlight, and a contrasting text color.
255
          itemStyle = themeData.accentTextTheme.body2;
256
          decoration = new BoxDecoration(
Hans Muller's avatar
Hans Muller committed
257
            backgroundColor: themeData.accentColor,
258
            shape: BoxShape.circle
259
          );
260 261
        } else if (disabled) {
          itemStyle = themeData.textTheme.body1.copyWith(color: themeData.disabledColor);
Hans Muller's avatar
Hans Muller committed
262 263
        } else if (currentDate.year == year && currentDate.month == month && currentDate.day == day) {
          // The current day gets a different text color.
264
          itemStyle = themeData.textTheme.body2.copyWith(color: themeData.accentColor);
Hans Muller's avatar
Hans Muller committed
265
        }
266

267 268 269
        Widget dayWidget = new Container(
          decoration: decoration,
          child: new Center(
270 271
            child: new Text(day.toString(), style: itemStyle),
          ),
272 273 274 275 276 277 278 279
        );

        if (!disabled) {
          dayWidget = new GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: () {
              onChanged(dayToBuild);
            },
280
            child: dayWidget,
281 282 283 284
          );
        }

        labels.add(dayWidget);
285 286 287
      }
    }

288 289 290 291 292 293 294 295
    return new Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: new Column(
        children: <Widget>[
          new Container(
            height: _kDayPickerRowHeight,
            child: new Center(
              child: new Text(new DateFormat('yMMMM').format(displayedMonth),
296 297 298
                style: themeData.textTheme.subhead,
              ),
            ),
299
          ),
300
          new Flexible(
301 302 303 304 305 306 307
            child: new GridView.custom(
              gridDelegate: _kDayPickerGridDelegate,
              childrenDelegate: new SliverChildListDelegate(labels, addRepaintBoundaries: false),
            ),
          ),
        ],
      ),
308
    );
309 310 311
  }
}

312 313 314 315 316
/// A scrollable list of months to allow picking a month.
///
/// Shows the days of each month in a rectangular grid with one column for each
/// day of the week.
///
317 318
/// The month picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
319 320
///
/// See also:
321
///
322
///  * [showDatePicker]
323
///  * <https://material.google.com/components/pickers.html#pickers-date-pickers>
324
class MonthPicker extends StatefulWidget {
325 326 327
  /// Creates a month picker.
  ///
  /// Rarely used directly. Instead, typically used as part of a [DatePicker].
328
  MonthPicker({
329
    Key key,
330 331 332
    @required this.selectedDate,
    @required this.onChanged,
    @required this.firstDate,
333
    @required this.lastDate,
334
    this.selectableDayPredicate,
335
  }) : super(key: key) {
336
    assert(selectedDate != null);
337
    assert(onChanged != null);
338
    assert(!firstDate.isAfter(lastDate));
Hans Muller's avatar
Hans Muller committed
339
    assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate));
340 341
  }

342 343 344
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
345
  final DateTime selectedDate;
346 347

  /// Called when the user picks a month.
Hixie's avatar
Hixie committed
348
  final ValueChanged<DateTime> onChanged;
349 350

  /// The earliest date the user is permitted to pick.
351
  final DateTime firstDate;
352 353

  /// The latest date the user is permitted to pick.
354
  final DateTime lastDate;
355

356 357 358
  /// Optional user supplied predicate function to customize selectable days.
  final SelectableDayPredicate selectableDayPredicate;

359
  @override
360
  _MonthPickerState createState() => new _MonthPickerState();
361
}
362

363
class _MonthPickerState extends State<MonthPicker> {
364
  @override
365 366
  void initState() {
    super.initState();
367
    // Initially display the pre-selected date.
368
    _dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate));
369
    _currentDisplayedMonthDate = new DateTime(config.selectedDate.year, config.selectedDate.month);
370
    _updateCurrentDate();
371 372
  }

373 374
  @override
  void didUpdateConfig(MonthPicker oldConfig) {
375
    if (config.selectedDate != oldConfig.selectedDate) {
376
      _dayPickerController = new PageController(initialPage: _monthDelta(config.firstDate, config.selectedDate));
377 378
      _currentDisplayedMonthDate =
          new DateTime(config.selectedDate.year, config.selectedDate.month);
379
    }
380 381
  }

382 383
  DateTime _todayDate;
  DateTime _currentDisplayedMonthDate;
384
  Timer _timer;
385
  PageController _dayPickerController;
386

387
  void _updateCurrentDate() {
388 389 390
    _todayDate = new DateTime.now();
    DateTime tomorrow = new DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
    Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
391
    timeUntilTomorrow += const Duration(seconds: 1);  // so we don't miss it by rounding
392 393 394
    if (_timer != null)
      _timer.cancel();
    _timer = new Timer(timeUntilTomorrow, () {
395 396 397 398 399 400
      setState(() {
        _updateCurrentDate();
      });
    });
  }

401
  static int _monthDelta(DateTime startDate, DateTime endDate) {
Hans Muller's avatar
Hans Muller committed
402 403 404
    return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
  }

405 406 407 408 409
  /// Add months to a month truncated date.
  DateTime _addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
    return new DateTime(monthDate.year + monthsToAdd ~/ 12, monthDate.month + monthsToAdd % 12);
  }

410 411 412 413 414 415 416 417 418 419 420 421
  Widget _buildItems(BuildContext context, int index) {
    final DateTime month = _addMonthsToMonthDate(config.firstDate, index);
    return new DayPicker(
      key: new ValueKey<DateTime>(month),
      selectedDate: config.selectedDate,
      currentDate: _todayDate,
      onChanged: config.onChanged,
      firstDate: config.firstDate,
      lastDate: config.lastDate,
      displayedMonth: month,
      selectableDayPredicate: config.selectableDayPredicate,
    );
422
  }
423

424
  void _handleNextMonth() {
425 426
    if (!_isDisplayingLastMonth)
      _dayPickerController.nextPage(duration: _kMonthScrollDuration, curve: Curves.ease);
427 428 429
  }

  void _handlePreviousMonth() {
430 431
    if (!_isDisplayingFirstMonth)
      _dayPickerController.previousPage(duration: _kMonthScrollDuration, curve: Curves.ease);
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
  }

  /// True if the earliest allowable month is displayed.
  bool get _isDisplayingFirstMonth {
    return !_currentDisplayedMonthDate.isAfter(
        new DateTime(config.firstDate.year, config.firstDate.month));
  }

  /// True if the latest allowable month is displayed.
  bool get _isDisplayingLastMonth {
    return !_currentDisplayedMonthDate.isBefore(
        new DateTime(config.lastDate.year, config.lastDate.month));
  }

  void _handleMonthPageChanged(int monthPage) {
    setState(() {
      _currentDisplayedMonthDate = _addMonthsToMonthDate(config.firstDate, monthPage);
    });
450 451
  }

452
  @override
453
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
454
    return new SizedBox(
455
      width: _kMonthPickerPortraitWidth,
456
      height: _kMaxDayPickerHeight,
Ian Hickson's avatar
Ian Hickson committed
457 458
      child: new Stack(
        children: <Widget>[
459 460 461
          new PageView.builder(
            key: new ValueKey<DateTime>(config.selectedDate),
            controller: _dayPickerController,
Ian Hickson's avatar
Ian Hickson committed
462 463
            scrollDirection: Axis.horizontal,
            itemCount: _monthDelta(config.firstDate, config.lastDate) + 1,
464
            itemBuilder: _buildItems,
465
            onPageChanged: _handleMonthPageChanged,
Ian Hickson's avatar
Ian Hickson committed
466 467 468 469 470 471 472
          ),
          new Positioned(
            top: 0.0,
            left: 8.0,
            child: new IconButton(
              icon: new Icon(Icons.chevron_left),
              tooltip: 'Previous month',
473 474
              onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
            ),
Ian Hickson's avatar
Ian Hickson committed
475 476 477 478 479 480 481
          ),
          new Positioned(
            top: 0.0,
            right: 8.0,
            child: new IconButton(
              icon: new Icon(Icons.chevron_right),
              tooltip: 'Next month',
482 483 484 485 486
              onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
            ),
          ),
        ],
      ),
487 488 489
    );
  }

490
  @override
491 492
  void dispose() {
    if (_timer != null)
493
      _timer.cancel();
494
    super.dispose();
495
  }
496 497
}

498 499
/// A scrollable list of years to allow picking a year.
///
500 501
/// The year picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
502 503 504 505
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
506
///
507
///  * [showDatePicker]
508
///  * <https://material.google.com/components/pickers.html#pickers-date-pickers>
509
class YearPicker extends StatefulWidget {
510 511 512 513 514 515
  /// Creates a year picker.
  ///
  /// The [selectedDate] and [onChanged] arguments must not be null. The
  /// [lastDate] must be after the [firstDate].
  ///
  /// Rarely used directly. Instead, typically used as part of a [DatePicker].
516
  YearPicker({
517
    Key key,
518 519 520
    @required this.selectedDate,
    @required this.onChanged,
    @required this.firstDate,
521
    @required this.lastDate,
522
  }) : super(key: key) {
523
    assert(selectedDate != null);
524
    assert(onChanged != null);
525
    assert(!firstDate.isAfter(lastDate));
526 527
  }

528 529 530
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
531
  final DateTime selectedDate;
532 533

  /// Called when the user picks a year.
Hixie's avatar
Hixie committed
534
  final ValueChanged<DateTime> onChanged;
535 536

  /// The earliest date the user is permitted to pick.
537
  final DateTime firstDate;
538 539

  /// The latest date the user is permitted to pick.
540 541
  final DateTime lastDate;

542
  @override
543
  _YearPickerState createState() => new _YearPickerState();
544 545
}

546 547
class _YearPickerState extends State<YearPicker> {
  static const double _itemExtent = 50.0;
548

549
  @override
550
  Widget build(BuildContext context) {
551
    assert(debugCheckHasMaterial(context));
552 553 554
    final ThemeData themeData = Theme.of(context);
    final TextStyle style = themeData.textTheme.body1;
    return new ListView.builder(
555 556
      itemExtent: _itemExtent,
      itemCount: config.lastDate.year - config.firstDate.year + 1,
557 558 559 560 561 562 563 564 565 566 567 568 569 570
      itemBuilder: (BuildContext context, int index) {
        final int year = config.firstDate.year + index;
        final TextStyle itemStyle = year == config.selectedDate.year ?
            themeData.textTheme.headline.copyWith(color: themeData.accentColor) : style;
        return new InkWell(
          key: new ValueKey<int>(year),
          onTap: () {
            config.onChanged(new DateTime(year, config.selectedDate.month, config.selectedDate.day));
          },
          child: new Center(
            child: new Text(year.toString(), style: itemStyle),
          ),
        );
      },
571 572
    );
  }
573
}
574 575 576 577 578 579

class _DatePickerDialog extends StatefulWidget {
  _DatePickerDialog({
    Key key,
    this.initialDate,
    this.firstDate,
580
    this.lastDate,
581
    this.selectableDayPredicate,
582 583 584 585 586
  }) : super(key: key);

  final DateTime initialDate;
  final DateTime firstDate;
  final DateTime lastDate;
587
  final SelectableDayPredicate selectableDayPredicate;
588 589 590 591 592 593 594 595 596 597 598 599 600 601

  @override
  _DatePickerDialogState createState() => new _DatePickerDialogState();
}

class _DatePickerDialogState extends State<_DatePickerDialog> {
  @override
  void initState() {
    super.initState();
    _selectedDate = config.initialDate;
  }

  DateTime _selectedDate;
  _DatePickerMode _mode = _DatePickerMode.day;
602
  GlobalKey _pickerKey = new GlobalKey();
603

604 605 606 607 608 609 610 611 612 613 614
  void _vibrate() {
    switch (Theme.of(context).platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
        HapticFeedback.vibrate();
        break;
      case TargetPlatform.iOS:
        break;
    }
  }

615
  void _handleModeChanged(_DatePickerMode mode) {
616
    _vibrate();
617 618 619 620 621 622
    setState(() {
      _mode = mode;
    });
  }

  void _handleYearChanged(DateTime value) {
623
    _vibrate();
624 625 626 627 628 629 630
    setState(() {
      _mode = _DatePickerMode.day;
      _selectedDate = value;
    });
  }

  void _handleDayChanged(DateTime value) {
631
    _vibrate();
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
    setState(() {
      _selectedDate = value;
    });
  }

  void _handleCancel() {
    Navigator.pop(context);
  }

  void _handleOk() {
    Navigator.pop(context, _selectedDate);
  }

  Widget _buildPicker() {
    assert(_mode != null);
    switch (_mode) {
      case _DatePickerMode.day:
        return new MonthPicker(
650
          key: _pickerKey,
651 652 653
          selectedDate: _selectedDate,
          onChanged: _handleDayChanged,
          firstDate: config.firstDate,
654
          lastDate: config.lastDate,
655
          selectableDayPredicate: config.selectableDayPredicate,
656 657 658
        );
      case _DatePickerMode.year:
        return new YearPicker(
659
          key: _pickerKey,
660 661 662
          selectedDate: _selectedDate,
          onChanged: _handleYearChanged,
          firstDate: config.firstDate,
663
          lastDate: config.lastDate,
664 665 666 667 668 669 670
        );
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
671
    Widget picker = new Flexible(
672
      child: new SizedBox(
673 674
        height: _kMaxDayPickerHeight,
        child: _buildPicker(),
675
      ),
676 677 678 679 680 681
    );
    Widget actions = new ButtonTheme.bar(
      child: new ButtonBar(
        children: <Widget>[
          new FlatButton(
            child: new Text('CANCEL'),
682
            onPressed: _handleCancel,
683 684 685
          ),
          new FlatButton(
            child: new Text('OK'),
686
            onPressed: _handleOk,
687
          ),
688 689
        ],
      ),
690 691 692 693 694 695 696 697 698
    );

    return new Dialog(
      child: new OrientationBuilder(
        builder: (BuildContext context, Orientation orientation) {
          Widget header = new _DatePickerHeader(
            selectedDate: _selectedDate,
            mode: _mode,
            onModeChanged: _handleModeChanged,
699
            orientation: orientation,
700 701 702 703 704 705 706 707 708
          );
          assert(orientation != null);
          switch (orientation) {
            case Orientation.portrait:
              return new SizedBox(
                width: _kMonthPickerPortraitWidth,
                child: new Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
709 710
                  children: <Widget>[header, picker, actions],
                ),
711 712 713 714 715 716 717 718 719
              );
            case Orientation.landscape:
              return new SizedBox(
                height: _kDatePickerLandscapeHeight,
                child: new Row(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    header,
720 721 722 723 724 725
                    new Flexible(
                      child: new SizedBox(
                        width: _kMonthPickerLandscapeWidth,
                        child: new Column(
                          mainAxisSize: MainAxisSize.min,
                          crossAxisAlignment: CrossAxisAlignment.stretch,
726 727 728 729 730 731
                          children: <Widget>[picker, actions],
                        ),
                      ),
                    ),
                  ],
                ),
732 733 734 735
              );
          }
          return null;
        }
736 737 738 739 740
      )
    );
  }
}

741 742 743 744 745
/// Signature for predicating dates for enabled date selections.
///
/// See [showDatePicker].
typedef bool SelectableDayPredicate(DateTime day);

746 747 748
/// Shows a dialog containing a material design date picker.
///
/// The returned [Future] resolves to the date selected by the user when the
749
/// user closes the dialog. If the user cancels the dialog, null is returned.
750
///
751 752 753 754
/// 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.
///
755 756 757
/// See also:
///
///  * [showTimePicker]
758
///  * <https://material.google.com/components/pickers.html#pickers-date-pickers>
759
Future<DateTime> showDatePicker({
760 761 762
  @required BuildContext context,
  @required DateTime initialDate,
  @required DateTime firstDate,
763
  @required DateTime lastDate,
764
  SelectableDayPredicate selectableDayPredicate,
765
}) async {
766 767 768 769 770 771 772
  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'
  );
773
  return await showDialog(
774 775 776 777
    context: context,
    child: new _DatePickerDialog(
      initialDate: initialDate,
      firstDate: firstDate,
778
      lastDate: lastDate,
779
      selectableDayPredicate: selectableDayPredicate,
780 781 782
    )
  );
}