customcalendardatepicker.dart 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335
  1. // Copyright 2014 The Flutter Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:math' as math;
  5. import 'package:flutter/gestures.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/rendering.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:flyinsonolite/controls/custom/customiconbutton.dart';
  10. import 'package:flyinsonolite/infrastructure/scale.dart';
  11. const Duration _monthScrollDuration = Duration(milliseconds: 200);
  12. double get _dayPickerRowHeight => 42.s;
  13. const int _maxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
  14. // One extra row for the day-of-week header.
  15. double get _maxDayPickerHeight =>
  16. _dayPickerRowHeight * (_maxDayPickerRowCount + 1);
  17. double get _monthPickerHorizontalPadding => 8.s;
  18. const int _yearPickerColumnCount = 3;
  19. double get _yearPickerPadding => 16.s;
  20. double get _yearPickerRowHeight => 52.s;
  21. double get _yearPickerRowSpacing => 8.s;
  22. double get _subHeaderHeight => 52.s;
  23. double get _monthNavButtonsWidth => 108.s;
  24. /// Displays a grid of days for a given month and allows the user to select a
  25. /// date.
  26. ///
  27. /// Days are arranged in a rectangular grid with one column for each day of the
  28. /// week. Controls are provided to change the year and month that the grid is
  29. /// showing.
  30. ///
  31. /// The calendar picker widget is rarely used directly. Instead, consider using
  32. /// [showCustomDatePicker], which will create a dialog that uses this as well as
  33. /// provides a text entry option.
  34. ///
  35. /// See also:
  36. ///
  37. /// * [showCustomDatePicker], which creates a Dialog that contains a
  38. /// [CustomCalendarDatePicker] and provides an optional compact view where the
  39. /// user can enter a date as a line of text.
  40. /// * [showCustomDatePicker], which shows a dialog that contains a Material Design
  41. /// time picker.
  42. ///
  43. class CustomCalendarDatePicker extends StatefulWidget {
  44. /// Creates a calendar date picker.
  45. ///
  46. /// It will display a grid of days for the [initialDate]'s month. The day
  47. /// indicated by [initialDate] will be selected.
  48. ///
  49. /// The optional [onDisplayedMonthChanged] callback can be used to track
  50. /// the currently displayed month.
  51. ///
  52. /// The user interface provides a way to change the year of the month being
  53. /// displayed. By default it will show the day grid, but this can be changed
  54. /// to start in the year selection interface with [initialCalendarMode] set
  55. /// to [DatePickerMode.year].
  56. ///
  57. /// The [initialDate], [firstDate], [lastDate], [onDateChanged], and
  58. /// [initialCalendarMode] must be non-null.
  59. ///
  60. /// [lastDate] must be after or equal to [firstDate].
  61. ///
  62. /// [initialDate] must be between [firstDate] and [lastDate] or equal to
  63. /// one of them.
  64. ///
  65. /// [currentDate] represents the current day (i.e. today). This
  66. /// date will be highlighted in the day grid. If null, the date of
  67. /// `DateTime.now()` will be used.
  68. ///
  69. /// If [selectableDayPredicate] is non-null, it must return `true` for the
  70. /// [initialDate].
  71. CustomCalendarDatePicker({
  72. super.key,
  73. required DateTime initialDate,
  74. required DateTime firstDate,
  75. required DateTime lastDate,
  76. DateTime? currentDate,
  77. required this.onDateChanged,
  78. this.onDisplayedMonthChanged,
  79. this.initialCalendarMode = DatePickerMode.day,
  80. this.selectableDayPredicate,
  81. }) : assert(initialDate != null),
  82. assert(firstDate != null),
  83. assert(lastDate != null),
  84. initialDate = DateUtils.dateOnly(initialDate),
  85. firstDate = DateUtils.dateOnly(firstDate),
  86. lastDate = DateUtils.dateOnly(lastDate),
  87. currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
  88. assert(onDateChanged != null),
  89. assert(initialCalendarMode != null) {
  90. assert(
  91. !this.lastDate.isBefore(this.firstDate),
  92. 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
  93. );
  94. assert(
  95. !this.initialDate.isBefore(this.firstDate),
  96. 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.',
  97. );
  98. assert(
  99. !this.initialDate.isAfter(this.lastDate),
  100. 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.',
  101. );
  102. assert(
  103. selectableDayPredicate == null ||
  104. selectableDayPredicate!(this.initialDate),
  105. 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.',
  106. );
  107. }
  108. /// The initially selected [DateTime] that the picker should display.
  109. final DateTime initialDate;
  110. /// The earliest allowable [DateTime] that the user can select.
  111. final DateTime firstDate;
  112. /// The latest allowable [DateTime] that the user can select.
  113. final DateTime lastDate;
  114. /// The [DateTime] representing today. It will be highlighted in the day grid.
  115. final DateTime currentDate;
  116. /// Called when the user selects a date in the picker.
  117. final ValueChanged<DateTime> onDateChanged;
  118. /// Called when the user navigates to a new month/year in the picker.
  119. final ValueChanged<DateTime>? onDisplayedMonthChanged;
  120. /// The initial display of the calendar picker.
  121. final DatePickerMode initialCalendarMode;
  122. /// Function to provide full control over which dates in the calendar can be selected.
  123. final SelectableDayPredicate? selectableDayPredicate;
  124. @override
  125. State<CustomCalendarDatePicker> createState() =>
  126. _CustomCalendarDatePickerState();
  127. }
  128. class _CustomCalendarDatePickerState extends State<CustomCalendarDatePicker> {
  129. bool _announcedInitialDate = false;
  130. late DatePickerMode _mode;
  131. late DateTime _currentDisplayedMonthDate;
  132. late DateTime _selectedDate;
  133. final GlobalKey _monthPickerKey = GlobalKey();
  134. final GlobalKey _yearPickerKey = GlobalKey();
  135. late MaterialLocalizations _localizations;
  136. late TextDirection _textDirection;
  137. @override
  138. void initState() {
  139. super.initState();
  140. _mode = widget.initialCalendarMode;
  141. _currentDisplayedMonthDate =
  142. DateTime(widget.initialDate.year, widget.initialDate.month);
  143. _selectedDate = widget.initialDate;
  144. }
  145. @override
  146. void didUpdateWidget(CustomCalendarDatePicker oldWidget) {
  147. super.didUpdateWidget(oldWidget);
  148. if (widget.initialCalendarMode != oldWidget.initialCalendarMode) {
  149. _mode = widget.initialCalendarMode;
  150. }
  151. if (!DateUtils.isSameDay(widget.initialDate, oldWidget.initialDate)) {
  152. _currentDisplayedMonthDate =
  153. DateTime(widget.initialDate.year, widget.initialDate.month);
  154. _selectedDate = widget.initialDate;
  155. }
  156. }
  157. @override
  158. void didChangeDependencies() {
  159. super.didChangeDependencies();
  160. assert(debugCheckHasMaterial(context));
  161. assert(debugCheckHasMaterialLocalizations(context));
  162. assert(debugCheckHasDirectionality(context));
  163. _localizations = MaterialLocalizations.of(context);
  164. _textDirection = Directionality.of(context);
  165. if (!_announcedInitialDate) {
  166. _announcedInitialDate = true;
  167. SemanticsService.announce(
  168. _localizations.formatFullDate(_selectedDate),
  169. _textDirection,
  170. );
  171. }
  172. }
  173. void _vibrate() {
  174. switch (Theme.of(context).platform) {
  175. case TargetPlatform.android:
  176. case TargetPlatform.fuchsia:
  177. case TargetPlatform.linux:
  178. case TargetPlatform.windows:
  179. HapticFeedback.vibrate();
  180. break;
  181. case TargetPlatform.iOS:
  182. case TargetPlatform.macOS:
  183. break;
  184. }
  185. }
  186. void _handleModeChanged(DatePickerMode mode) {
  187. _vibrate();
  188. setState(() {
  189. _mode = mode;
  190. if (_mode == DatePickerMode.day) {
  191. SemanticsService.announce(
  192. _localizations.formatMonthYear(_selectedDate),
  193. _textDirection,
  194. );
  195. } else {
  196. SemanticsService.announce(
  197. _localizations.formatYear(_selectedDate),
  198. _textDirection,
  199. );
  200. }
  201. });
  202. }
  203. void _handleMonthChanged(DateTime date) {
  204. setState(() {
  205. if (_currentDisplayedMonthDate.year != date.year ||
  206. _currentDisplayedMonthDate.month != date.month) {
  207. _currentDisplayedMonthDate = DateTime(date.year, date.month);
  208. widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate);
  209. }
  210. });
  211. }
  212. void _handleYearChanged(DateTime value) {
  213. _vibrate();
  214. if (value.isBefore(widget.firstDate)) {
  215. value = widget.firstDate;
  216. } else if (value.isAfter(widget.lastDate)) {
  217. value = widget.lastDate;
  218. }
  219. setState(() {
  220. _mode = DatePickerMode.day;
  221. _handleMonthChanged(value);
  222. });
  223. }
  224. void _handleDayChanged(DateTime value) {
  225. _vibrate();
  226. setState(() {
  227. _selectedDate = value;
  228. widget.onDateChanged(_selectedDate);
  229. });
  230. }
  231. Widget _buildPicker() {
  232. switch (_mode) {
  233. case DatePickerMode.day:
  234. return _MonthPicker(
  235. key: _monthPickerKey,
  236. initialMonth: _currentDisplayedMonthDate,
  237. currentDate: widget.currentDate,
  238. firstDate: widget.firstDate,
  239. lastDate: widget.lastDate,
  240. selectedDate: _selectedDate,
  241. onChanged: _handleDayChanged,
  242. onDisplayedMonthChanged: _handleMonthChanged,
  243. selectableDayPredicate: widget.selectableDayPredicate,
  244. );
  245. case DatePickerMode.year:
  246. return Padding(
  247. padding: EdgeInsets.only(top: _subHeaderHeight),
  248. child: YearPicker(
  249. key: _yearPickerKey,
  250. currentDate: widget.currentDate,
  251. firstDate: widget.firstDate,
  252. lastDate: widget.lastDate,
  253. initialDate: _currentDisplayedMonthDate,
  254. selectedDate: _selectedDate,
  255. onChanged: _handleYearChanged,
  256. ),
  257. );
  258. }
  259. }
  260. @override
  261. Widget build(BuildContext context) {
  262. assert(debugCheckHasMaterial(context));
  263. assert(debugCheckHasMaterialLocalizations(context));
  264. assert(debugCheckHasDirectionality(context));
  265. return Stack(
  266. children: <Widget>[
  267. SizedBox(
  268. height: _subHeaderHeight + _maxDayPickerHeight,
  269. child: _buildPicker(),
  270. ),
  271. // Put the mode toggle button on top so that it won't be covered up by the _MonthPicker
  272. _DatePickerModeToggleButton(
  273. mode: _mode,
  274. title: _localizations.formatMonthYear(_currentDisplayedMonthDate),
  275. onTitlePressed: () {
  276. // Toggle the day/year mode.
  277. _handleModeChanged(_mode == DatePickerMode.day
  278. ? DatePickerMode.year
  279. : DatePickerMode.day);
  280. },
  281. ),
  282. ],
  283. );
  284. }
  285. }
  286. /// A button that used to toggle the [DatePickerMode] for a date picker.
  287. ///
  288. /// This appears above the calendar grid and allows the user to toggle the
  289. /// [DatePickerMode] to display either the calendar view or the year list.
  290. class _DatePickerModeToggleButton extends StatefulWidget {
  291. const _DatePickerModeToggleButton({
  292. required this.mode,
  293. required this.title,
  294. required this.onTitlePressed,
  295. });
  296. /// The current display of the calendar picker.
  297. final DatePickerMode mode;
  298. /// The text that displays the current month/year being viewed.
  299. final String title;
  300. /// The callback when the title is pressed.
  301. final VoidCallback onTitlePressed;
  302. @override
  303. _DatePickerModeToggleButtonState createState() =>
  304. _DatePickerModeToggleButtonState();
  305. }
  306. class _DatePickerModeToggleButtonState
  307. extends State<_DatePickerModeToggleButton>
  308. with SingleTickerProviderStateMixin {
  309. late AnimationController _controller;
  310. @override
  311. void initState() {
  312. super.initState();
  313. _controller = AnimationController(
  314. value: widget.mode == DatePickerMode.year ? 0.5 : 0,
  315. upperBound: 0.5,
  316. duration: const Duration(milliseconds: 200),
  317. vsync: this,
  318. );
  319. }
  320. @override
  321. void didUpdateWidget(_DatePickerModeToggleButton oldWidget) {
  322. super.didUpdateWidget(oldWidget);
  323. if (oldWidget.mode == widget.mode) {
  324. return;
  325. }
  326. if (widget.mode == DatePickerMode.year) {
  327. _controller.forward();
  328. } else {
  329. _controller.reverse();
  330. }
  331. }
  332. @override
  333. Widget build(BuildContext context) {
  334. final ColorScheme colorScheme = Theme.of(context).colorScheme;
  335. final TextTheme textTheme = Theme.of(context).textTheme;
  336. final Color controlColor = colorScheme.onSurface.withOpacity(0.60);
  337. return Container(
  338. padding: EdgeInsetsDirectional.only(start: 16.s, end: 4.s),
  339. height: _subHeaderHeight,
  340. child: Row(
  341. children: <Widget>[
  342. Flexible(
  343. child: Semantics(
  344. label: MaterialLocalizations.of(context).selectYearSemanticsLabel,
  345. excludeSemantics: true,
  346. button: true,
  347. child: SizedBox(
  348. height: _subHeaderHeight,
  349. child: InkWell(
  350. onTap: widget.onTitlePressed,
  351. child: Padding(
  352. padding: EdgeInsets.symmetric(horizontal: 8.s),
  353. child: Row(
  354. children: <Widget>[
  355. Flexible(
  356. child: Text(
  357. widget.title,
  358. overflow: TextOverflow.ellipsis,
  359. style: textTheme.titleSmall?.copyWith(
  360. color: controlColor,
  361. ),
  362. ),
  363. ),
  364. RotationTransition(
  365. turns: _controller,
  366. child: Icon(
  367. Icons.arrow_drop_down,
  368. color: controlColor,
  369. size: 24.s,
  370. ),
  371. ),
  372. ],
  373. ),
  374. ),
  375. ),
  376. ),
  377. ),
  378. ),
  379. if (widget.mode == DatePickerMode.day)
  380. // Give space for the prev/next month buttons that are underneath this row
  381. SizedBox(width: _monthNavButtonsWidth),
  382. ],
  383. ),
  384. );
  385. }
  386. @override
  387. void dispose() {
  388. _controller.dispose();
  389. super.dispose();
  390. }
  391. }
  392. class _MonthPicker extends StatefulWidget {
  393. /// Creates a month picker.
  394. _MonthPicker({
  395. super.key,
  396. required this.initialMonth,
  397. required this.currentDate,
  398. required this.firstDate,
  399. required this.lastDate,
  400. required this.selectedDate,
  401. required this.onChanged,
  402. required this.onDisplayedMonthChanged,
  403. this.selectableDayPredicate,
  404. }) : assert(selectedDate != null),
  405. assert(currentDate != null),
  406. assert(onChanged != null),
  407. assert(firstDate != null),
  408. assert(lastDate != null),
  409. assert(!firstDate.isAfter(lastDate)),
  410. assert(!selectedDate.isBefore(firstDate)),
  411. assert(!selectedDate.isAfter(lastDate));
  412. /// The initial month to display.
  413. final DateTime initialMonth;
  414. /// The current date.
  415. ///
  416. /// This date is subtly highlighted in the picker.
  417. final DateTime currentDate;
  418. /// The earliest date the user is permitted to pick.
  419. ///
  420. /// This date must be on or before the [lastDate].
  421. final DateTime firstDate;
  422. /// The latest date the user is permitted to pick.
  423. ///
  424. /// This date must be on or after the [firstDate].
  425. final DateTime lastDate;
  426. /// The currently selected date.
  427. ///
  428. /// This date is highlighted in the picker.
  429. final DateTime selectedDate;
  430. /// Called when the user picks a day.
  431. final ValueChanged<DateTime> onChanged;
  432. /// Called when the user navigates to a new month.
  433. final ValueChanged<DateTime> onDisplayedMonthChanged;
  434. /// Optional user supplied predicate function to customize selectable days.
  435. final SelectableDayPredicate? selectableDayPredicate;
  436. @override
  437. _MonthPickerState createState() => _MonthPickerState();
  438. }
  439. class _MonthPickerState extends State<_MonthPicker> {
  440. final GlobalKey _pageViewKey = GlobalKey();
  441. late DateTime _currentMonth;
  442. late PageController _pageController;
  443. late MaterialLocalizations _localizations;
  444. late TextDirection _textDirection;
  445. Map<ShortcutActivator, Intent>? _shortcutMap;
  446. Map<Type, Action<Intent>>? _actionMap;
  447. late FocusNode _dayGridFocus;
  448. DateTime? _focusedDay;
  449. @override
  450. void initState() {
  451. super.initState();
  452. _currentMonth = widget.initialMonth;
  453. _pageController = PageController(
  454. initialPage: DateUtils.monthDelta(widget.firstDate, _currentMonth));
  455. _shortcutMap = const <ShortcutActivator, Intent>{
  456. SingleActivator(LogicalKeyboardKey.arrowLeft):
  457. DirectionalFocusIntent(TraversalDirection.left),
  458. SingleActivator(LogicalKeyboardKey.arrowRight):
  459. DirectionalFocusIntent(TraversalDirection.right),
  460. SingleActivator(LogicalKeyboardKey.arrowDown):
  461. DirectionalFocusIntent(TraversalDirection.down),
  462. SingleActivator(LogicalKeyboardKey.arrowUp):
  463. DirectionalFocusIntent(TraversalDirection.up),
  464. };
  465. _actionMap = <Type, Action<Intent>>{
  466. NextFocusIntent:
  467. CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
  468. PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(
  469. onInvoke: _handleGridPreviousFocus),
  470. DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(
  471. onInvoke: _handleDirectionFocus),
  472. };
  473. _dayGridFocus = FocusNode(debugLabel: 'Day Grid');
  474. }
  475. @override
  476. void didChangeDependencies() {
  477. super.didChangeDependencies();
  478. _localizations = MaterialLocalizations.of(context);
  479. _textDirection = Directionality.of(context);
  480. }
  481. @override
  482. void didUpdateWidget(_MonthPicker oldWidget) {
  483. super.didUpdateWidget(oldWidget);
  484. if (widget.initialMonth != oldWidget.initialMonth &&
  485. widget.initialMonth != _currentMonth) {
  486. // We can't interrupt this widget build with a scroll, so do it next frame
  487. WidgetsBinding.instance.addPostFrameCallback(
  488. (Duration timeStamp) => _showMonth(widget.initialMonth, jump: true),
  489. );
  490. }
  491. }
  492. @override
  493. void dispose() {
  494. _pageController.dispose();
  495. _dayGridFocus.dispose();
  496. super.dispose();
  497. }
  498. void _handleDateSelected(DateTime selectedDate) {
  499. _focusedDay = selectedDate;
  500. widget.onChanged(selectedDate);
  501. }
  502. void _handleMonthPageChanged(int monthPage) {
  503. setState(() {
  504. final DateTime monthDate =
  505. DateUtils.addMonthsToMonthDate(widget.firstDate, monthPage);
  506. if (!DateUtils.isSameMonth(_currentMonth, monthDate)) {
  507. _currentMonth = DateTime(monthDate.year, monthDate.month);
  508. widget.onDisplayedMonthChanged(_currentMonth);
  509. if (_focusedDay != null &&
  510. !DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
  511. // We have navigated to a new month with the grid focused, but the
  512. // focused day is not in this month. Choose a new one trying to keep
  513. // the same day of the month.
  514. _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day);
  515. }
  516. SemanticsService.announce(
  517. _localizations.formatMonthYear(_currentMonth),
  518. _textDirection,
  519. );
  520. }
  521. });
  522. }
  523. /// Returns a focusable date for the given month.
  524. ///
  525. /// If the preferredDay is available in the month it will be returned,
  526. /// otherwise the first selectable day in the month will be returned. If
  527. /// no dates are selectable in the month, then it will return null.
  528. DateTime? _focusableDayForMonth(DateTime month, int preferredDay) {
  529. final int daysInMonth = DateUtils.getDaysInMonth(month.year, month.month);
  530. // Can we use the preferred day in this month?
  531. if (preferredDay <= daysInMonth) {
  532. final DateTime newFocus = DateTime(month.year, month.month, preferredDay);
  533. if (_isSelectable(newFocus)) {
  534. return newFocus;
  535. }
  536. }
  537. // Start at the 1st and take the first selectable date.
  538. for (int day = 1; day <= daysInMonth; day++) {
  539. final DateTime newFocus = DateTime(month.year, month.month, day);
  540. if (_isSelectable(newFocus)) {
  541. return newFocus;
  542. }
  543. }
  544. return null;
  545. }
  546. /// Navigate to the next month.
  547. void _handleNextMonth() {
  548. if (!_isDisplayingLastMonth) {
  549. _pageController.nextPage(
  550. duration: _monthScrollDuration,
  551. curve: Curves.ease,
  552. );
  553. }
  554. }
  555. /// Navigate to the previous month.
  556. void _handlePreviousMonth() {
  557. if (!_isDisplayingFirstMonth) {
  558. _pageController.previousPage(
  559. duration: _monthScrollDuration,
  560. curve: Curves.ease,
  561. );
  562. }
  563. }
  564. /// Navigate to the given month.
  565. void _showMonth(DateTime month, {bool jump = false}) {
  566. final int monthPage = DateUtils.monthDelta(widget.firstDate, month);
  567. if (jump) {
  568. _pageController.jumpToPage(monthPage);
  569. } else {
  570. _pageController.animateToPage(
  571. monthPage,
  572. duration: _monthScrollDuration,
  573. curve: Curves.ease,
  574. );
  575. }
  576. }
  577. /// True if the earliest allowable month is displayed.
  578. bool get _isDisplayingFirstMonth {
  579. return !_currentMonth.isAfter(
  580. DateTime(widget.firstDate.year, widget.firstDate.month),
  581. );
  582. }
  583. /// True if the latest allowable month is displayed.
  584. bool get _isDisplayingLastMonth {
  585. return !_currentMonth.isBefore(
  586. DateTime(widget.lastDate.year, widget.lastDate.month),
  587. );
  588. }
  589. /// Handler for when the overall day grid obtains or loses focus.
  590. void _handleGridFocusChange(bool focused) {
  591. setState(() {
  592. if (focused && _focusedDay == null) {
  593. if (DateUtils.isSameMonth(widget.selectedDate, _currentMonth)) {
  594. _focusedDay = widget.selectedDate;
  595. } else if (DateUtils.isSameMonth(widget.currentDate, _currentMonth)) {
  596. _focusedDay =
  597. _focusableDayForMonth(_currentMonth, widget.currentDate.day);
  598. } else {
  599. _focusedDay = _focusableDayForMonth(_currentMonth, 1);
  600. }
  601. }
  602. });
  603. }
  604. /// Move focus to the next element after the day grid.
  605. void _handleGridNextFocus(NextFocusIntent intent) {
  606. _dayGridFocus.requestFocus();
  607. _dayGridFocus.nextFocus();
  608. }
  609. /// Move focus to the previous element before the day grid.
  610. void _handleGridPreviousFocus(PreviousFocusIntent intent) {
  611. _dayGridFocus.requestFocus();
  612. _dayGridFocus.previousFocus();
  613. }
  614. /// Move the internal focus date in the direction of the given intent.
  615. ///
  616. /// This will attempt to move the focused day to the next selectable day in
  617. /// the given direction. If the new date is not in the current month, then
  618. /// the page view will be scrolled to show the new date's month.
  619. ///
  620. /// For horizontal directions, it will move forward or backward a day (depending
  621. /// on the current [TextDirection]). For vertical directions it will move up and
  622. /// down a week at a time.
  623. void _handleDirectionFocus(DirectionalFocusIntent intent) {
  624. assert(_focusedDay != null);
  625. setState(() {
  626. final DateTime? nextDate =
  627. _nextDateInDirection(_focusedDay!, intent.direction);
  628. if (nextDate != null) {
  629. _focusedDay = nextDate;
  630. if (!DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
  631. _showMonth(_focusedDay!);
  632. }
  633. }
  634. });
  635. }
  636. static const Map<TraversalDirection, int> _directionOffset =
  637. <TraversalDirection, int>{
  638. TraversalDirection.up: -DateTime.daysPerWeek,
  639. TraversalDirection.right: 1,
  640. TraversalDirection.down: DateTime.daysPerWeek,
  641. TraversalDirection.left: -1,
  642. };
  643. int _dayDirectionOffset(
  644. TraversalDirection traversalDirection, TextDirection textDirection) {
  645. // Swap left and right if the text direction if RTL
  646. if (textDirection == TextDirection.rtl) {
  647. if (traversalDirection == TraversalDirection.left) {
  648. traversalDirection = TraversalDirection.right;
  649. } else if (traversalDirection == TraversalDirection.right) {
  650. traversalDirection = TraversalDirection.left;
  651. }
  652. }
  653. return _directionOffset[traversalDirection]!;
  654. }
  655. DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
  656. final TextDirection textDirection = Directionality.of(context);
  657. DateTime nextDate = DateUtils.addDaysToDate(
  658. date, _dayDirectionOffset(direction, textDirection));
  659. while (!nextDate.isBefore(widget.firstDate) &&
  660. !nextDate.isAfter(widget.lastDate)) {
  661. if (_isSelectable(nextDate)) {
  662. return nextDate;
  663. }
  664. nextDate = DateUtils.addDaysToDate(
  665. nextDate, _dayDirectionOffset(direction, textDirection));
  666. }
  667. return null;
  668. }
  669. bool _isSelectable(DateTime date) {
  670. return widget.selectableDayPredicate == null ||
  671. widget.selectableDayPredicate!.call(date);
  672. }
  673. Widget _buildItems(BuildContext context, int index) {
  674. final DateTime month =
  675. DateUtils.addMonthsToMonthDate(widget.firstDate, index);
  676. return _DayPicker(
  677. key: ValueKey<DateTime>(month),
  678. selectedDate: widget.selectedDate,
  679. currentDate: widget.currentDate,
  680. onChanged: _handleDateSelected,
  681. firstDate: widget.firstDate,
  682. lastDate: widget.lastDate,
  683. displayedMonth: month,
  684. selectableDayPredicate: widget.selectableDayPredicate,
  685. );
  686. }
  687. @override
  688. Widget build(BuildContext context) {
  689. final Color controlColor =
  690. Theme.of(context).colorScheme.onSurface.withOpacity(0.60);
  691. return Semantics(
  692. child: Column(
  693. children: <Widget>[
  694. Container(
  695. padding: EdgeInsetsDirectional.only(start: 16.s, end: 4.s),
  696. height: _subHeaderHeight,
  697. child: Row(
  698. children: <Widget>[
  699. const Spacer(),
  700. CustomIconButton(
  701. icon: const Icon(
  702. Icons.chevron_left,
  703. ),
  704. padding: EdgeInsets.all(8.s),
  705. iconSize: 30.s,
  706. constraints: const BoxConstraints(),
  707. color: controlColor,
  708. tooltip: _isDisplayingFirstMonth
  709. ? null
  710. : _localizations.previousMonthTooltip,
  711. onPressed:
  712. _isDisplayingFirstMonth ? null : _handlePreviousMonth,
  713. ),
  714. CustomIconButton(
  715. icon: const Icon(
  716. Icons.chevron_right,
  717. ),
  718. padding: EdgeInsets.all(8.s),
  719. iconSize: 30.s,
  720. constraints: const BoxConstraints(),
  721. color: controlColor,
  722. tooltip: _isDisplayingLastMonth
  723. ? null
  724. : _localizations.nextMonthTooltip,
  725. onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
  726. ),
  727. ],
  728. ),
  729. ),
  730. Expanded(
  731. child: FocusableActionDetector(
  732. shortcuts: _shortcutMap,
  733. actions: _actionMap,
  734. focusNode: _dayGridFocus,
  735. onFocusChange: _handleGridFocusChange,
  736. child: _FocusedDate(
  737. date: _dayGridFocus.hasFocus ? _focusedDay : null,
  738. child: PageView.builder(
  739. key: _pageViewKey,
  740. controller: _pageController,
  741. itemBuilder: _buildItems,
  742. itemCount:
  743. DateUtils.monthDelta(widget.firstDate, widget.lastDate) +
  744. 1,
  745. onPageChanged: _handleMonthPageChanged,
  746. ),
  747. ),
  748. ),
  749. ),
  750. ],
  751. ),
  752. );
  753. }
  754. }
  755. /// InheritedWidget indicating what the current focused date is for its children.
  756. ///
  757. /// This is used by the [_MonthPicker] to let its children [_DayPicker]s know
  758. /// what the currently focused date (if any) should be.
  759. class _FocusedDate extends InheritedWidget {
  760. const _FocusedDate({
  761. required super.child,
  762. this.date,
  763. });
  764. final DateTime? date;
  765. @override
  766. bool updateShouldNotify(_FocusedDate oldWidget) {
  767. return !DateUtils.isSameDay(date, oldWidget.date);
  768. }
  769. static DateTime? of(BuildContext context) {
  770. final _FocusedDate? focusedDate =
  771. context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
  772. return focusedDate?.date;
  773. }
  774. }
  775. /// Displays the days of a given month and allows choosing a day.
  776. ///
  777. /// The days are arranged in a rectangular grid with one column for each day of
  778. /// the week.
  779. class _DayPicker extends StatefulWidget {
  780. /// Creates a day picker.
  781. _DayPicker({
  782. super.key,
  783. required this.currentDate,
  784. required this.displayedMonth,
  785. required this.firstDate,
  786. required this.lastDate,
  787. required this.selectedDate,
  788. required this.onChanged,
  789. this.selectableDayPredicate,
  790. }) : assert(currentDate != null),
  791. assert(displayedMonth != null),
  792. assert(firstDate != null),
  793. assert(lastDate != null),
  794. assert(selectedDate != null),
  795. assert(onChanged != null),
  796. assert(!firstDate.isAfter(lastDate)),
  797. assert(!selectedDate.isBefore(firstDate)),
  798. assert(!selectedDate.isAfter(lastDate));
  799. /// The currently selected date.
  800. ///
  801. /// This date is highlighted in the picker.
  802. final DateTime selectedDate;
  803. /// The current date at the time the picker is displayed.
  804. final DateTime currentDate;
  805. /// Called when the user picks a day.
  806. final ValueChanged<DateTime> onChanged;
  807. /// The earliest date the user is permitted to pick.
  808. ///
  809. /// This date must be on or before the [lastDate].
  810. final DateTime firstDate;
  811. /// The latest date the user is permitted to pick.
  812. ///
  813. /// This date must be on or after the [firstDate].
  814. final DateTime lastDate;
  815. /// The month whose days are displayed by this picker.
  816. final DateTime displayedMonth;
  817. /// Optional user supplied predicate function to customize selectable days.
  818. final SelectableDayPredicate? selectableDayPredicate;
  819. @override
  820. _DayPickerState createState() => _DayPickerState();
  821. }
  822. class _DayPickerState extends State<_DayPicker> {
  823. /// List of [FocusNode]s, one for each day of the month.
  824. late List<FocusNode> _dayFocusNodes;
  825. @override
  826. void initState() {
  827. super.initState();
  828. final int daysInMonth = DateUtils.getDaysInMonth(
  829. widget.displayedMonth.year, widget.displayedMonth.month);
  830. _dayFocusNodes = List<FocusNode>.generate(
  831. daysInMonth,
  832. (int index) =>
  833. FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'),
  834. );
  835. }
  836. @override
  837. void didChangeDependencies() {
  838. super.didChangeDependencies();
  839. // Check to see if the focused date is in this month, if so focus it.
  840. final DateTime? focusedDate = _FocusedDate.of(context);
  841. if (focusedDate != null &&
  842. DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
  843. _dayFocusNodes[focusedDate.day - 1].requestFocus();
  844. }
  845. }
  846. @override
  847. void dispose() {
  848. for (final FocusNode node in _dayFocusNodes) {
  849. node.dispose();
  850. }
  851. super.dispose();
  852. }
  853. /// Builds widgets showing abbreviated days of week. The first widget in the
  854. /// returned list corresponds to the first day of week for the current locale.
  855. ///
  856. /// Examples:
  857. ///
  858. /// ┌ Sunday is the first day of week in the US (en_US)
  859. /// |
  860. /// S M T W T F S ← the returned list contains these widgets
  861. /// _ _ _ _ _ 1 2
  862. /// 3 4 5 6 7 8 9
  863. ///
  864. /// ┌ But it's Monday in the UK (en_GB)
  865. /// |
  866. /// M T W T F S S ← the returned list contains these widgets
  867. /// _ _ _ _ 1 2 3
  868. /// 4 5 6 7 8 9 10
  869. ///
  870. List<Widget> _dayHeaders(
  871. TextStyle? headerStyle, MaterialLocalizations localizations) {
  872. final List<Widget> result = <Widget>[];
  873. for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
  874. final String weekday = localizations.narrowWeekdays[i];
  875. result.add(ExcludeSemantics(
  876. child: Center(child: Text(weekday, style: headerStyle)),
  877. ));
  878. if (i == (localizations.firstDayOfWeekIndex - 1) % 7) {
  879. break;
  880. }
  881. }
  882. return result;
  883. }
  884. @override
  885. Widget build(BuildContext context) {
  886. final ColorScheme colorScheme = Theme.of(context).colorScheme;
  887. final MaterialLocalizations localizations =
  888. MaterialLocalizations.of(context);
  889. final TextTheme textTheme = Theme.of(context).textTheme;
  890. final TextStyle? headerStyle = textTheme.bodySmall?.apply(
  891. color: colorScheme.onSurface.withOpacity(0.60),
  892. );
  893. final TextStyle dayStyle = textTheme.bodySmall!;
  894. final Color enabledDayColor = colorScheme.onSurface.withOpacity(0.87);
  895. final Color disabledDayColor = colorScheme.onSurface.withOpacity(0.38);
  896. final Color selectedDayColor = colorScheme.onPrimary;
  897. final Color selectedDayBackground = colorScheme.primary;
  898. final Color todayColor = colorScheme.primary;
  899. final int year = widget.displayedMonth.year;
  900. final int month = widget.displayedMonth.month;
  901. final int daysInMonth = DateUtils.getDaysInMonth(year, month);
  902. final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
  903. final List<Widget> dayItems = _dayHeaders(headerStyle, localizations);
  904. // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
  905. // a leap year.
  906. int day = -dayOffset;
  907. while (day < daysInMonth) {
  908. day++;
  909. if (day < 1) {
  910. dayItems.add(Container());
  911. } else {
  912. final DateTime dayToBuild = DateTime(year, month, day);
  913. final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
  914. dayToBuild.isBefore(widget.firstDate) ||
  915. (widget.selectableDayPredicate != null &&
  916. !widget.selectableDayPredicate!(dayToBuild));
  917. final bool isSelectedDay =
  918. DateUtils.isSameDay(widget.selectedDate, dayToBuild);
  919. final bool isToday =
  920. DateUtils.isSameDay(widget.currentDate, dayToBuild);
  921. BoxDecoration? decoration;
  922. Color dayColor = enabledDayColor;
  923. if (isSelectedDay) {
  924. // The selected day gets a circle background highlight, and a
  925. // contrasting text color.
  926. dayColor = selectedDayColor;
  927. decoration = BoxDecoration(
  928. color: selectedDayBackground,
  929. shape: BoxShape.circle,
  930. );
  931. } else if (isDisabled) {
  932. dayColor = disabledDayColor;
  933. } else if (isToday) {
  934. // The current day gets a different text color and a circle stroke
  935. // border.
  936. dayColor = todayColor;
  937. decoration = BoxDecoration(
  938. border: Border.all(color: todayColor, width: 1.s),
  939. shape: BoxShape.circle,
  940. );
  941. }
  942. Widget dayWidget = Container(
  943. decoration: decoration,
  944. child: Center(
  945. child: Text(localizations.formatDecimal(day),
  946. style: dayStyle.apply(color: dayColor)),
  947. ),
  948. );
  949. if (isDisabled) {
  950. dayWidget = ExcludeSemantics(
  951. child: dayWidget,
  952. );
  953. } else {
  954. dayWidget = InkResponse(
  955. focusNode: _dayFocusNodes[day - 1],
  956. onTap: () => widget.onChanged(dayToBuild),
  957. radius: _dayPickerRowHeight / 2 + 4.s,
  958. splashColor: selectedDayBackground.withOpacity(0.38),
  959. child: Semantics(
  960. // We want the day of month to be spoken first irrespective of the
  961. // locale-specific preferences or TextDirection. This is because
  962. // an accessibility user is more likely to be interested in the
  963. // day of month before the rest of the date, as they are looking
  964. // for the day of month. To do that we prepend day of month to the
  965. // formatted full date.
  966. label:
  967. '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}',
  968. selected: isSelectedDay,
  969. excludeSemantics: true,
  970. child: dayWidget,
  971. ),
  972. );
  973. }
  974. dayItems.add(dayWidget);
  975. }
  976. }
  977. return Padding(
  978. padding: EdgeInsets.symmetric(
  979. horizontal: _monthPickerHorizontalPadding,
  980. ),
  981. child: GridView.custom(
  982. physics: const ClampingScrollPhysics(),
  983. gridDelegate: _dayPickerGridDelegate,
  984. childrenDelegate: SliverChildListDelegate(
  985. dayItems,
  986. addRepaintBoundaries: false,
  987. ),
  988. ),
  989. );
  990. }
  991. }
  992. class _DayPickerGridDelegate extends SliverGridDelegate {
  993. const _DayPickerGridDelegate();
  994. @override
  995. SliverGridLayout getLayout(SliverConstraints constraints) {
  996. const int columnCount = DateTime.daysPerWeek;
  997. final double tileWidth = constraints.crossAxisExtent / columnCount;
  998. final double tileHeight = math.min(
  999. _dayPickerRowHeight,
  1000. constraints.viewportMainAxisExtent / (_maxDayPickerRowCount + 1),
  1001. );
  1002. return SliverGridRegularTileLayout(
  1003. childCrossAxisExtent: tileWidth,
  1004. childMainAxisExtent: tileHeight,
  1005. crossAxisCount: columnCount,
  1006. crossAxisStride: tileWidth,
  1007. mainAxisStride: tileHeight,
  1008. reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
  1009. );
  1010. }
  1011. @override
  1012. bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false;
  1013. }
  1014. const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate();
  1015. /// A scrollable grid of years to allow picking a year.
  1016. ///
  1017. /// The year picker widget is rarely used directly. Instead, consider using
  1018. /// [CustomCalendarDatePicker], or [showCustomDatePicker] which create full date pickers.
  1019. ///
  1020. /// See also:
  1021. ///
  1022. /// * [CustomCalendarDatePicker], which provides a Material Design date picker
  1023. /// interface.
  1024. ///
  1025. /// * [showCustomDatePicker], which shows a dialog containing a Material Design
  1026. /// date picker.
  1027. ///
  1028. class YearPicker extends StatefulWidget {
  1029. /// Creates a year picker.
  1030. ///
  1031. /// The [firstDate], [lastDate], [selectedDate], and [onChanged]
  1032. /// arguments must be non-null. The [lastDate] must be after the [firstDate].
  1033. YearPicker({
  1034. super.key,
  1035. DateTime? currentDate,
  1036. required this.firstDate,
  1037. required this.lastDate,
  1038. DateTime? initialDate,
  1039. required this.selectedDate,
  1040. required this.onChanged,
  1041. this.dragStartBehavior = DragStartBehavior.start,
  1042. }) : assert(firstDate != null),
  1043. assert(lastDate != null),
  1044. assert(selectedDate != null),
  1045. assert(onChanged != null),
  1046. assert(!firstDate.isAfter(lastDate)),
  1047. currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
  1048. initialDate = DateUtils.dateOnly(initialDate ?? selectedDate);
  1049. /// The current date.
  1050. ///
  1051. /// This date is subtly highlighted in the picker.
  1052. final DateTime currentDate;
  1053. /// The earliest date the user is permitted to pick.
  1054. final DateTime firstDate;
  1055. /// The latest date the user is permitted to pick.
  1056. final DateTime lastDate;
  1057. /// The initial date to center the year display around.
  1058. final DateTime initialDate;
  1059. /// The currently selected date.
  1060. ///
  1061. /// This date is highlighted in the picker.
  1062. final DateTime selectedDate;
  1063. /// Called when the user picks a year.
  1064. final ValueChanged<DateTime> onChanged;
  1065. /// {@macro flutter.widgets.scrollable.dragStartBehavior}
  1066. final DragStartBehavior dragStartBehavior;
  1067. @override
  1068. State<YearPicker> createState() => _YearPickerState();
  1069. }
  1070. class _YearPickerState extends State<YearPicker> {
  1071. late ScrollController _scrollController;
  1072. // The approximate number of years necessary to fill the available space.
  1073. static const int minYears = 18;
  1074. @override
  1075. void initState() {
  1076. super.initState();
  1077. _scrollController = ScrollController(
  1078. initialScrollOffset: _scrollOffsetForYear(widget.selectedDate));
  1079. }
  1080. @override
  1081. void didUpdateWidget(YearPicker oldWidget) {
  1082. super.didUpdateWidget(oldWidget);
  1083. if (widget.selectedDate != oldWidget.selectedDate) {
  1084. _scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate));
  1085. }
  1086. }
  1087. double _scrollOffsetForYear(DateTime date) {
  1088. final int initialYearIndex = date.year - widget.firstDate.year;
  1089. final int initialYearRow = initialYearIndex ~/ _yearPickerColumnCount;
  1090. // Move the offset down by 2 rows to approximately center it.
  1091. final int centeredYearRow = initialYearRow - 2;
  1092. return _itemCount < minYears ? 0 : centeredYearRow * _yearPickerRowHeight;
  1093. }
  1094. Widget _buildYearItem(BuildContext context, int index) {
  1095. final ColorScheme colorScheme = Theme.of(context).colorScheme;
  1096. final TextTheme textTheme = Theme.of(context).textTheme;
  1097. // Backfill the _YearPicker with disabled years if necessary.
  1098. final int offset = _itemCount < minYears ? (minYears - _itemCount) ~/ 2 : 0;
  1099. final int year = widget.firstDate.year + index - offset;
  1100. final bool isSelected = year == widget.selectedDate.year;
  1101. final bool isCurrentYear = year == widget.currentDate.year;
  1102. final bool isDisabled =
  1103. year < widget.firstDate.year || year > widget.lastDate.year;
  1104. double decorationHeight = 36.s;
  1105. double decorationWidth = 72.s;
  1106. final Color textColor;
  1107. if (isSelected) {
  1108. textColor = colorScheme.onPrimary;
  1109. } else if (isDisabled) {
  1110. textColor = colorScheme.onSurface.withOpacity(0.38);
  1111. } else if (isCurrentYear) {
  1112. textColor = colorScheme.primary;
  1113. } else {
  1114. textColor = colorScheme.onSurface.withOpacity(0.87);
  1115. }
  1116. final TextStyle? itemStyle = textTheme.bodyLarge?.apply(color: textColor);
  1117. BoxDecoration? decoration;
  1118. if (isSelected) {
  1119. decoration = BoxDecoration(
  1120. color: colorScheme.primary,
  1121. borderRadius: BorderRadius.circular(decorationHeight / 2),
  1122. );
  1123. } else if (isCurrentYear && !isDisabled) {
  1124. decoration = BoxDecoration(
  1125. border: Border.all(color: colorScheme.primary, width: 1.s),
  1126. borderRadius: BorderRadius.circular(decorationHeight / 2),
  1127. );
  1128. }
  1129. Widget yearItem = Center(
  1130. child: Container(
  1131. decoration: decoration,
  1132. height: decorationHeight,
  1133. width: decorationWidth,
  1134. child: Center(
  1135. child: Semantics(
  1136. selected: isSelected,
  1137. button: true,
  1138. child: Text(year.toString(), style: itemStyle),
  1139. ),
  1140. ),
  1141. ),
  1142. );
  1143. if (isDisabled) {
  1144. yearItem = ExcludeSemantics(
  1145. child: yearItem,
  1146. );
  1147. } else {
  1148. yearItem = InkWell(
  1149. key: ValueKey<int>(year),
  1150. onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month)),
  1151. child: yearItem,
  1152. );
  1153. }
  1154. return yearItem;
  1155. }
  1156. int get _itemCount {
  1157. return widget.lastDate.year - widget.firstDate.year + 1;
  1158. }
  1159. @override
  1160. Widget build(BuildContext context) {
  1161. assert(debugCheckHasMaterial(context));
  1162. return Column(
  1163. children: <Widget>[
  1164. const Divider(),
  1165. Expanded(
  1166. child: GridView.builder(
  1167. controller: _scrollController,
  1168. dragStartBehavior: widget.dragStartBehavior,
  1169. gridDelegate: _yearPickerGridDelegate,
  1170. itemBuilder: _buildYearItem,
  1171. itemCount: math.max(_itemCount, minYears),
  1172. padding: EdgeInsets.symmetric(horizontal: _yearPickerPadding),
  1173. ),
  1174. ),
  1175. const Divider(),
  1176. ],
  1177. );
  1178. }
  1179. }
  1180. class _YearPickerGridDelegate extends SliverGridDelegate {
  1181. const _YearPickerGridDelegate();
  1182. @override
  1183. SliverGridLayout getLayout(SliverConstraints constraints) {
  1184. final double tileWidth = (constraints.crossAxisExtent -
  1185. (_yearPickerColumnCount - 1) * _yearPickerRowSpacing) /
  1186. _yearPickerColumnCount;
  1187. return SliverGridRegularTileLayout(
  1188. childCrossAxisExtent: tileWidth,
  1189. childMainAxisExtent: _yearPickerRowHeight,
  1190. crossAxisCount: _yearPickerColumnCount,
  1191. crossAxisStride: tileWidth + _yearPickerRowSpacing,
  1192. mainAxisStride: _yearPickerRowHeight,
  1193. reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
  1194. );
  1195. }
  1196. @override
  1197. bool shouldRelayout(_YearPickerGridDelegate oldDelegate) => false;
  1198. }
  1199. const _YearPickerGridDelegate _yearPickerGridDelegate =
  1200. _YearPickerGridDelegate();