date_picker_deprecated.dart 21.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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/gestures.dart' show DragStartBehavior;
9
import 'package:flutter/rendering.dart';
10
import 'package:flutter/widgets.dart';
11

12 13 14 15 16
import 'date.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'material_localizations.dart';
import 'theme.dart';
17

18
// This is the original implementation for the Material Date Picker.
19 20 21 22
// These classes are deprecated and the whole file can be removed after
// this has been on stable for long enough for people to migrate to the new
// CalendarDatePicker (if needed, as showDatePicker has already been migrated
// and it is what most apps would have used).
xster's avatar
xster committed
23

24
const Duration _kMonthScrollDuration = Duration(milliseconds: 200);
25 26 27 28
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);
29

30 31 32
class _DayPickerGridDelegate extends SliverGridDelegate {
  const _DayPickerGridDelegate();

33
  @override
34
  SliverGridLayout getLayout(SliverConstraints constraints) {
35
    const int columnCount = DateTime.daysPerWeek;
36
    final double tileWidth = constraints.crossAxisExtent / columnCount;
37 38
    final double viewTileHeight = constraints.viewportMainAxisExtent / (_kMaxDayPickerRowCount + 1);
    final double tileHeight = math.max(_kDayPickerRowHeight, viewTileHeight);
39
    return SliverGridRegularTileLayout(
40 41 42 43 44
      crossAxisCount: columnCount,
      mainAxisStride: tileHeight,
      crossAxisStride: tileWidth,
      childMainAxisExtent: tileHeight,
      childCrossAxisExtent: tileWidth,
45
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
46 47
    );
  }
48 49 50

  @override
  bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false;
51 52
}

53
const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate();
54

55 56 57 58 59
/// 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.
///
60 61
/// The day picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
62 63
///
/// See also:
64
///
65 66 67 68
///  * [showDatePicker], which shows a dialog that contains a material design
///    date picker.
///  * [showTimePicker], which shows a dialog that contains a material design
///    time picker.
69 70 71
///
@Deprecated(
  'Use CalendarDatePicker instead. '
72
  'This feature was deprecated after v1.26.0-18.0.pre.'
73
)
74
class DayPicker extends StatelessWidget {
75 76
  /// Creates a day picker.
  ///
77
  /// Rarely used directly. Instead, typically used as part of a [MonthPicker].
78 79 80 81
  @Deprecated(
    'Use CalendarDatePicker instead. '
    'This feature was deprecated after v1.26.0-18.0.pre.'
  )
82
  DayPicker({
83 84 85 86 87 88 89
    Key? key,
    required this.selectedDate,
    required this.currentDate,
    required this.onChanged,
    required this.firstDate,
    required this.lastDate,
    required this.displayedMonth,
90
    this.selectableDayPredicate,
91
    this.dragStartBehavior = DragStartBehavior.start,
92 93 94 95
  }) : assert(selectedDate != null),
       assert(currentDate != null),
       assert(onChanged != null),
       assert(displayedMonth != null),
96
       assert(dragStartBehavior != null),
97 98 99
       assert(!firstDate.isAfter(lastDate)),
       assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)),
       super(key: key);
100

101 102 103
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
104
  final DateTime selectedDate;
105 106

  /// The current date at the time the picker is displayed.
107
  final DateTime currentDate;
108 109

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

112 113 114 115 116 117
  /// The earliest date the user is permitted to pick.
  final DateTime firstDate;

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

118
  /// The month whose days are displayed by this picker.
119 120
  final DateTime displayedMonth;

121
  /// Optional user supplied predicate function to customize selectable days.
122
  final SelectableDayPredicate? selectableDayPredicate;
123

124 125 126 127 128 129 130 131 132 133 134
  /// Determines the way that drag start behavior is handled.
  ///
  /// If set to [DragStartBehavior.start], the drag gesture used to scroll a
  /// date picker wheel will begin upon the detection of a drag gesture. If set
  /// to [DragStartBehavior.down] it will begin when a down event is first
  /// detected.
  ///
  /// In general, setting this to [DragStartBehavior.start] will make drag
  /// animation smoother and setting it to [DragStartBehavior.down] will make
  /// drag behavior feel slightly more reactive.
  ///
135
  /// By default, the drag start behavior is [DragStartBehavior.start].
136 137 138 139 140 141
  ///
  /// See also:
  ///
  ///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
  final DragStartBehavior dragStartBehavior;

Yegor's avatar
Yegor committed
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
  /// Builds widgets showing abbreviated days of week. The first widget in the
  /// returned list corresponds to the first day of week for the current locale.
  ///
  /// Examples:
  ///
  /// ```
  /// ┌ Sunday is the first day of week in the US (en_US)
  /// |
  /// S M T W T F S  <-- the returned list contains these widgets
  /// _ _ _ _ _ 1 2
  /// 3 4 5 6 7 8 9
  ///
  /// ┌ But it's Monday in the UK (en_GB)
  /// |
  /// M T W T F S S  <-- the returned list contains these widgets
  /// _ _ _ _ 1 2 3
  /// 4 5 6 7 8 9 10
  /// ```
160
  List<Widget> _getDayHeaders(TextStyle? headerStyle, MaterialLocalizations localizations) {
Yegor's avatar
Yegor committed
161 162
    final List<Widget> result = <Widget>[];
    for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
163
      final String weekday = localizations.narrowWeekdays[i];
164 165
      result.add(ExcludeSemantics(
        child: Center(child: Text(weekday, style: headerStyle)),
166
      ));
Yegor's avatar
Yegor committed
167 168 169 170
      if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
        break;
    }
    return result;
171 172
  }

173
  // Do not use this directly - call getDaysInMonth instead.
174
  static const List<int> _daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
175

176 177 178 179 180
  /// Returns the number of days in a month, according to the proleptic
  /// Gregorian calendar.
  ///
  /// This applies the leap year logic introduced by the Gregorian reforms of
  /// 1582. It will not give valid results for dates prior to that time.
181
  static int getDaysInMonth(int year, int month) {
182
    if (month == DateTime.february) {
183 184 185
      final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
      if (isLeapYear)
        return 29;
186
      return 28;
187
    }
188
    return _daysInMonth[month - 1];
189 190
  }

Yegor's avatar
Yegor committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
  /// Computes the offset from the first day of week that the first day of the
  /// [month] falls on.
  ///
  /// For example, September 1, 2017 falls on a Friday, which in the calendar
  /// localized for United States English appears as:
  ///
  /// ```
  /// S M T W T F S
  /// _ _ _ _ _ 1 2
  /// ```
  ///
  /// The offset for the first day of the months is the number of leading blanks
  /// in the calendar, i.e. 5.
  ///
  /// The same date localized for the Russian calendar has a different offset,
  /// because the first day of week is Monday rather than Sunday:
  ///
  /// ```
  /// M T W T F S S
  /// _ _ _ _ 1 2 3
  /// ```
  ///
  /// So the offset is 4, rather than 5.
  ///
  /// This code consolidates the following:
  ///
  /// - [DateTime.weekday] provides a 1-based index into days of week, with 1
  ///   falling on Monday.
  /// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
220 221
  ///   into the [MaterialLocalizations.narrowWeekdays] list.
  /// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
Yegor's avatar
Yegor committed
222 223 224
  ///   days of week, always starting with Sunday and ending with Saturday.
  int _computeFirstDayOffset(int year, int month, MaterialLocalizations localizations) {
    // 0-based day of week, with 0 representing Monday.
225
    final int weekdayFromMonday = DateTime(year, month).weekday - 1;
Yegor's avatar
Yegor committed
226 227 228 229 230 231
    // 0-based day of week, with 0 representing Sunday.
    final int firstDayOfWeekFromSunday = localizations.firstDayOfWeekIndex;
    // firstDayOfWeekFromSunday recomputed to be Monday-based
    final int firstDayOfWeekFromMonday = (firstDayOfWeekFromSunday - 1) % 7;
    // Number of days between the first day of week appearing on the calendar,
    // and the day corresponding to the 1-st of the month.
232
    return (weekdayFromMonday - firstDayOfWeekFromMonday) % 7;
Yegor's avatar
Yegor committed
233 234
  }

235
  @override
236
  Widget build(BuildContext context) {
237
    final ThemeData themeData = Theme.of(context);
238
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
239 240
    final int year = displayedMonth.year;
    final int month = displayedMonth.month;
241
    final int daysInMonth = getDaysInMonth(year, month);
Yegor's avatar
Yegor committed
242
    final int firstDayOffset = _computeFirstDayOffset(year, month, localizations);
243 244 245
    final List<Widget> labels = <Widget>[
      ..._getDayHeaders(themeData.textTheme.caption, localizations),
    ];
Yegor's avatar
Yegor committed
246 247 248 249
    for (int i = 0; true; i += 1) {
      // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
      // a leap year.
      final int day = i - firstDayOffset + 1;
250 251 252
      if (day > daysInMonth)
        break;
      if (day < 1) {
253
        labels.add(Container());
254
      } else {
255
        final DateTime dayToBuild = DateTime(year, month, day);
256 257
        final bool disabled = dayToBuild.isAfter(lastDate)
            || dayToBuild.isBefore(firstDate)
258
            || (selectableDayPredicate != null && !selectableDayPredicate!(dayToBuild));
259

260 261
        BoxDecoration? decoration;
        TextStyle? itemStyle = themeData.textTheme.bodyText2;
Hans Muller's avatar
Hans Muller committed
262

263 264
        final bool isSelectedDay = selectedDate.year == year && selectedDate.month == month && selectedDate.day == day;
        if (isSelectedDay) {
Hans Muller's avatar
Hans Muller committed
265
          // The selected day gets a circle background highlight, and a contrasting text color.
266
          itemStyle = themeData.accentTextTheme.bodyText1;
267
          decoration = BoxDecoration(
268
            color: themeData.accentColor,
269
            shape: BoxShape.circle,
270
          );
271
        } else if (disabled) {
272
          itemStyle = themeData.textTheme.bodyText2!.copyWith(color: themeData.disabledColor);
Hans Muller's avatar
Hans Muller committed
273 274
        } else if (currentDate.year == year && currentDate.month == month && currentDate.day == day) {
          // The current day gets a different text color.
275
          itemStyle = themeData.textTheme.bodyText1!.copyWith(color: themeData.accentColor);
Hans Muller's avatar
Hans Muller committed
276
        }
277

278
        Widget dayWidget = Container(
279
          decoration: decoration,
280 281
          child: Center(
            child: Semantics(
282 283 284 285 286 287 288 289
              // We want the day of month to be spoken first irrespective of the
              // locale-specific preferences or TextDirection. This is because
              // an accessibility user is more likely to be interested in the
              // day of month before the rest of the date, as they are looking
              // for the day of month. To do that we prepend day of month to the
              // formatted full date.
              label: '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}',
              selected: isSelectedDay,
290
              sortKey: OrdinalSortKey(day.toDouble()),
291 292
              child: ExcludeSemantics(
                child: Text(localizations.formatDecimal(day), style: itemStyle),
293 294
              ),
            ),
295
          ),
296 297 298
        );

        if (!disabled) {
299
          dayWidget = GestureDetector(
300 301 302 303
            behavior: HitTestBehavior.opaque,
            onTap: () {
              onChanged(dayToBuild);
            },
304
            child: dayWidget,
305
            dragStartBehavior: dragStartBehavior,
306 307 308 309
          );
        }

        labels.add(dayWidget);
310 311 312
      }
    }

313
    return Padding(
314
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
315
      child: Column(
316
        children: <Widget>[
317
          SizedBox(
318
            height: _kDayPickerRowHeight,
319 320 321
            child: Center(
              child: ExcludeSemantics(
                child: Text(
322
                  localizations.formatMonthYear(displayedMonth),
323
                  style: themeData.textTheme.subtitle1,
324
                ),
325 326
              ),
            ),
327
          ),
328 329
          Flexible(
            child: GridView.custom(
330
              gridDelegate: _kDayPickerGridDelegate,
331
              childrenDelegate: SliverChildListDelegate(labels, addRepaintBoundaries: false),
332
              padding: EdgeInsets.zero,
333 334 335 336
            ),
          ),
        ],
      ),
337
    );
338 339 340
  }
}

341 342 343 344 345
/// 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.
///
346 347
/// The month picker widget is rarely used directly. Instead, consider using
/// [showDatePicker], which creates a date picker dialog.
348 349
///
/// See also:
350
///
351 352 353 354
///  * [showDatePicker], which shows a dialog that contains a material design
///    date picker.
///  * [showTimePicker], which shows a dialog that contains a material design
///    time picker.
355 356 357
///
@Deprecated(
  'Use CalendarDatePicker instead. '
358
  'This feature was deprecated after v1.26.0-18.0.pre.'
359
)
360
class MonthPicker extends StatefulWidget {
361 362
  /// Creates a month picker.
  ///
363 364
  /// Rarely used directly. Instead, typically used as part of the dialog shown
  /// by [showDatePicker].
365 366 367 368
  @Deprecated(
    'Use CalendarDatePicker instead. '
    'This feature was deprecated after v1.26.0-18.0.pre.'
  )
369
  MonthPicker({
370 371 372 373 374
    Key? key,
    required this.selectedDate,
    required this.onChanged,
    required this.firstDate,
    required this.lastDate,
375
    this.selectableDayPredicate,
376
    this.dragStartBehavior = DragStartBehavior.start,
377 378 379 380 381
  }) : assert(selectedDate != null),
       assert(onChanged != null),
       assert(!firstDate.isAfter(lastDate)),
       assert(selectedDate.isAfter(firstDate) || selectedDate.isAtSameMomentAs(firstDate)),
       super(key: key);
382

383 384 385
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
386
  final DateTime selectedDate;
387 388

  /// Called when the user picks a month.
Hixie's avatar
Hixie committed
389
  final ValueChanged<DateTime> onChanged;
390 391

  /// The earliest date the user is permitted to pick.
392
  final DateTime firstDate;
393 394

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

397
  /// Optional user supplied predicate function to customize selectable days.
398
  final SelectableDayPredicate? selectableDayPredicate;
399

400 401 402
  /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  final DragStartBehavior dragStartBehavior;

403
  @override
404
  _MonthPickerState createState() => _MonthPickerState();
405
}
406

407
class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin {
408 409 410
  static final Animatable<double> _chevronOpacityTween = Tween<double>(begin: 1.0, end: 0.0)
    .chain(CurveTween(curve: Curves.easeInOut));

411
  @override
412 413
  void initState() {
    super.initState();
414
    // Initially display the pre-selected date.
415
    final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
416
    _dayPickerController = PageController(initialPage: monthPage);
417
    _handleMonthPageChanged(monthPage);
418
    _updateCurrentDate();
419 420

    // Setup the fade animation for chevrons
421
    _chevronOpacityController = AnimationController(
422
      duration: const Duration(milliseconds: 250), vsync: this,
423
    );
424
    _chevronOpacityAnimation = _chevronOpacityController.drive(_chevronOpacityTween);
425 426
  }

427
  @override
428
  void didUpdateWidget(MonthPicker oldWidget) {
429
    super.didUpdateWidget(oldWidget);
430
    if (widget.selectedDate != oldWidget.selectedDate) {
431
      final int monthPage = _monthDelta(widget.firstDate, widget.selectedDate);
432
      _dayPickerController = PageController(initialPage: monthPage);
433
      _handleMonthPageChanged(monthPage);
434
    }
435 436
  }

437 438
  MaterialLocalizations? localizations;
  TextDirection? textDirection;
439 440 441 442 443 444 445 446

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    localizations = MaterialLocalizations.of(context);
    textDirection = Directionality.of(context);
  }

447 448 449 450 451 452
  late DateTime _todayDate;
  late DateTime _currentDisplayedMonthDate;
  Timer? _timer;
  late PageController _dayPickerController;
  late AnimationController _chevronOpacityController;
  late Animation<double> _chevronOpacityAnimation;
453

454
  void _updateCurrentDate() {
455 456
    _todayDate = DateTime.now();
    final DateTime tomorrow = DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
457
    Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
458
    timeUntilTomorrow += const Duration(seconds: 1); // so we don't miss it by rounding
459
    _timer?.cancel();
460
    _timer = Timer(timeUntilTomorrow, () {
461 462 463 464 465 466
      setState(() {
        _updateCurrentDate();
      });
    });
  }

467
  static int _monthDelta(DateTime startDate, DateTime endDate) {
Hans Muller's avatar
Hans Muller committed
468 469 470
    return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
  }

471 472
  /// Add months to a month truncated date.
  DateTime _addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
473
    return DateTime(monthDate.year + monthsToAdd ~/ 12, monthDate.month + monthsToAdd % 12);
474 475
  }

476
  Widget _buildItems(BuildContext context, int index) {
477
    final DateTime month = _addMonthsToMonthDate(widget.firstDate, index);
478 479
    return DayPicker(
      key: ValueKey<DateTime>(month),
480
      selectedDate: widget.selectedDate,
481
      currentDate: _todayDate,
482 483 484
      onChanged: widget.onChanged,
      firstDate: widget.firstDate,
      lastDate: widget.lastDate,
485
      displayedMonth: month,
486
      selectableDayPredicate: widget.selectableDayPredicate,
487
      dragStartBehavior: widget.dragStartBehavior,
488
    );
489
  }
490

491
  void _handleNextMonth() {
492
    if (!_isDisplayingLastMonth) {
493
      SemanticsService.announce(localizations!.formatMonthYear(_nextMonthDate), textDirection!);
494
      _dayPickerController.nextPage(duration: _kMonthScrollDuration, curve: Curves.ease);
495
    }
496 497 498
  }

  void _handlePreviousMonth() {
499
    if (!_isDisplayingFirstMonth) {
500
      SemanticsService.announce(localizations!.formatMonthYear(_previousMonthDate), textDirection!);
501
      _dayPickerController.previousPage(duration: _kMonthScrollDuration, curve: Curves.ease);
502
    }
503 504 505 506 507
  }

  /// True if the earliest allowable month is displayed.
  bool get _isDisplayingFirstMonth {
    return !_currentDisplayedMonthDate.isAfter(
508
        DateTime(widget.firstDate.year, widget.firstDate.month));
509 510 511 512 513
  }

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

517 518
  late DateTime _previousMonthDate;
  late DateTime _nextMonthDate;
519

520 521
  void _handleMonthPageChanged(int monthPage) {
    setState(() {
522
      _previousMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage - 1);
523
      _currentDisplayedMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage);
524
      _nextMonthDate = _addMonthsToMonthDate(widget.firstDate, monthPage + 1);
525
    });
526 527
  }

528
  @override
529
  Widget build(BuildContext context) {
530
    return SizedBox(
531 532
      // The month picker just adds month navigation to the day picker, so make
      // it the same height as the DayPicker
533
      height: _kMaxDayPickerHeight,
534
      child: Stack(
Ian Hickson's avatar
Ian Hickson committed
535
        children: <Widget>[
536
          Semantics(
537
            sortKey: _MonthPickerSortKey.calendar,
538
            child: NotificationListener<ScrollStartNotification>(
539 540 541 542
              onNotification: (_) {
                _chevronOpacityController.forward();
                return false;
              },
543
              child: NotificationListener<ScrollEndNotification>(
544 545 546 547
                onNotification: (_) {
                  _chevronOpacityController.reverse();
                  return false;
                },
548
                child: PageView.builder(
549
                  dragStartBehavior: widget.dragStartBehavior,
550
                  key: ValueKey<DateTime>(widget.selectedDate),
551 552 553 554 555 556 557
                  controller: _dayPickerController,
                  scrollDirection: Axis.horizontal,
                  itemCount: _monthDelta(widget.firstDate, widget.lastDate) + 1,
                  itemBuilder: _buildItems,
                  onPageChanged: _handleMonthPageChanged,
                ),
              ),
558
            ),
Ian Hickson's avatar
Ian Hickson committed
559
          ),
560
          PositionedDirectional(
Ian Hickson's avatar
Ian Hickson committed
561
            top: 0.0,
562
            start: 8.0,
563
            child: Semantics(
564
              sortKey: _MonthPickerSortKey.previousMonth,
565
              child: FadeTransition(
566
                opacity: _chevronOpacityAnimation,
567
                child: IconButton(
568
                  icon: const Icon(Icons.chevron_left),
569
                  tooltip: _isDisplayingFirstMonth ? null : '${localizations!.previousMonthTooltip} ${localizations!.formatMonthYear(_previousMonthDate)}',
570 571
                  onPressed: _isDisplayingFirstMonth ? null : _handlePreviousMonth,
                ),
572
              ),
573
            ),
Ian Hickson's avatar
Ian Hickson committed
574
          ),
575
          PositionedDirectional(
Ian Hickson's avatar
Ian Hickson committed
576
            top: 0.0,
577
            end: 8.0,
578
            child: Semantics(
579
              sortKey: _MonthPickerSortKey.nextMonth,
580
              child: FadeTransition(
581
                opacity: _chevronOpacityAnimation,
582
                child: IconButton(
583
                  icon: const Icon(Icons.chevron_right),
584
                  tooltip: _isDisplayingLastMonth ? null : '${localizations!.nextMonthTooltip} ${localizations!.formatMonthYear(_nextMonthDate)}',
585 586
                  onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
                ),
587
              ),
588 589 590 591
            ),
          ),
        ],
      ),
592 593 594
    );
  }

595
  @override
596
  void dispose() {
597
    _timer?.cancel();
598 599
    _chevronOpacityController.dispose();
    _dayPickerController.dispose();
600
    super.dispose();
601
  }
602 603
}

604 605 606
// Defines semantic traversal order of the top-level widgets inside the month
// picker.
class _MonthPickerSortKey extends OrdinalSortKey {
607 608
  const _MonthPickerSortKey(double order) : super(order);

609 610 611
  static const _MonthPickerSortKey previousMonth = _MonthPickerSortKey(1.0);
  static const _MonthPickerSortKey nextMonth = _MonthPickerSortKey(2.0);
  static const _MonthPickerSortKey calendar = _MonthPickerSortKey(3.0);
612
}