123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335 |
- // Copyright 2014 The Flutter 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:math' as math;
- import 'package:flutter/gestures.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/services.dart';
- import 'package:flyinsonolite/controls/custom/customiconbutton.dart';
- import 'package:flyinsonolite/infrastructure/scale.dart';
- const Duration _monthScrollDuration = Duration(milliseconds: 200);
- double get _dayPickerRowHeight => 42.s;
- const int _maxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
- // One extra row for the day-of-week header.
- double get _maxDayPickerHeight =>
- _dayPickerRowHeight * (_maxDayPickerRowCount + 1);
- double get _monthPickerHorizontalPadding => 8.s;
- const int _yearPickerColumnCount = 3;
- double get _yearPickerPadding => 16.s;
- double get _yearPickerRowHeight => 52.s;
- double get _yearPickerRowSpacing => 8.s;
- double get _subHeaderHeight => 52.s;
- double get _monthNavButtonsWidth => 108.s;
- /// Displays a grid of days for a given month and allows the user to select a
- /// date.
- ///
- /// Days are arranged in a rectangular grid with one column for each day of the
- /// week. Controls are provided to change the year and month that the grid is
- /// showing.
- ///
- /// The calendar picker widget is rarely used directly. Instead, consider using
- /// [showCustomDatePicker], which will create a dialog that uses this as well as
- /// provides a text entry option.
- ///
- /// See also:
- ///
- /// * [showCustomDatePicker], which creates a Dialog that contains a
- /// [CustomCalendarDatePicker] and provides an optional compact view where the
- /// user can enter a date as a line of text.
- /// * [showCustomDatePicker], which shows a dialog that contains a Material Design
- /// time picker.
- ///
- class CustomCalendarDatePicker extends StatefulWidget {
- /// Creates a calendar date picker.
- ///
- /// It will display a grid of days for the [initialDate]'s month. The day
- /// indicated by [initialDate] will be selected.
- ///
- /// The optional [onDisplayedMonthChanged] callback can be used to track
- /// the currently displayed month.
- ///
- /// The user interface provides a way to change the year of the month being
- /// displayed. By default it will show the day grid, but this can be changed
- /// to start in the year selection interface with [initialCalendarMode] set
- /// to [DatePickerMode.year].
- ///
- /// The [initialDate], [firstDate], [lastDate], [onDateChanged], and
- /// [initialCalendarMode] must be non-null.
- ///
- /// [lastDate] must be after or equal to [firstDate].
- ///
- /// [initialDate] must be between [firstDate] and [lastDate] or equal to
- /// one of them.
- ///
- /// [currentDate] represents the current day (i.e. today). This
- /// date will be highlighted in the day grid. If null, the date of
- /// `DateTime.now()` will be used.
- ///
- /// If [selectableDayPredicate] is non-null, it must return `true` for the
- /// [initialDate].
- CustomCalendarDatePicker({
- super.key,
- required DateTime initialDate,
- required DateTime firstDate,
- required DateTime lastDate,
- DateTime? currentDate,
- required this.onDateChanged,
- this.onDisplayedMonthChanged,
- this.initialCalendarMode = DatePickerMode.day,
- this.selectableDayPredicate,
- }) : assert(initialDate != null),
- assert(firstDate != null),
- assert(lastDate != null),
- initialDate = DateUtils.dateOnly(initialDate),
- firstDate = DateUtils.dateOnly(firstDate),
- lastDate = DateUtils.dateOnly(lastDate),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
- assert(onDateChanged != null),
- assert(initialCalendarMode != null) {
- assert(
- !this.lastDate.isBefore(this.firstDate),
- 'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
- );
- assert(
- !this.initialDate.isBefore(this.firstDate),
- 'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.',
- );
- assert(
- !this.initialDate.isAfter(this.lastDate),
- 'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.',
- );
- assert(
- selectableDayPredicate == null ||
- selectableDayPredicate!(this.initialDate),
- 'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.',
- );
- }
- /// The initially selected [DateTime] that the picker should display.
- final DateTime initialDate;
- /// The earliest allowable [DateTime] that the user can select.
- final DateTime firstDate;
- /// The latest allowable [DateTime] that the user can select.
- final DateTime lastDate;
- /// The [DateTime] representing today. It will be highlighted in the day grid.
- final DateTime currentDate;
- /// Called when the user selects a date in the picker.
- final ValueChanged<DateTime> onDateChanged;
- /// Called when the user navigates to a new month/year in the picker.
- final ValueChanged<DateTime>? onDisplayedMonthChanged;
- /// The initial display of the calendar picker.
- final DatePickerMode initialCalendarMode;
- /// Function to provide full control over which dates in the calendar can be selected.
- final SelectableDayPredicate? selectableDayPredicate;
- @override
- State<CustomCalendarDatePicker> createState() =>
- _CustomCalendarDatePickerState();
- }
- class _CustomCalendarDatePickerState extends State<CustomCalendarDatePicker> {
- bool _announcedInitialDate = false;
- late DatePickerMode _mode;
- late DateTime _currentDisplayedMonthDate;
- late DateTime _selectedDate;
- final GlobalKey _monthPickerKey = GlobalKey();
- final GlobalKey _yearPickerKey = GlobalKey();
- late MaterialLocalizations _localizations;
- late TextDirection _textDirection;
- @override
- void initState() {
- super.initState();
- _mode = widget.initialCalendarMode;
- _currentDisplayedMonthDate =
- DateTime(widget.initialDate.year, widget.initialDate.month);
- _selectedDate = widget.initialDate;
- }
- @override
- void didUpdateWidget(CustomCalendarDatePicker oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.initialCalendarMode != oldWidget.initialCalendarMode) {
- _mode = widget.initialCalendarMode;
- }
- if (!DateUtils.isSameDay(widget.initialDate, oldWidget.initialDate)) {
- _currentDisplayedMonthDate =
- DateTime(widget.initialDate.year, widget.initialDate.month);
- _selectedDate = widget.initialDate;
- }
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- assert(debugCheckHasMaterial(context));
- assert(debugCheckHasMaterialLocalizations(context));
- assert(debugCheckHasDirectionality(context));
- _localizations = MaterialLocalizations.of(context);
- _textDirection = Directionality.of(context);
- if (!_announcedInitialDate) {
- _announcedInitialDate = true;
- SemanticsService.announce(
- _localizations.formatFullDate(_selectedDate),
- _textDirection,
- );
- }
- }
- void _vibrate() {
- switch (Theme.of(context).platform) {
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- HapticFeedback.vibrate();
- break;
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- break;
- }
- }
- void _handleModeChanged(DatePickerMode mode) {
- _vibrate();
- setState(() {
- _mode = mode;
- if (_mode == DatePickerMode.day) {
- SemanticsService.announce(
- _localizations.formatMonthYear(_selectedDate),
- _textDirection,
- );
- } else {
- SemanticsService.announce(
- _localizations.formatYear(_selectedDate),
- _textDirection,
- );
- }
- });
- }
- void _handleMonthChanged(DateTime date) {
- setState(() {
- if (_currentDisplayedMonthDate.year != date.year ||
- _currentDisplayedMonthDate.month != date.month) {
- _currentDisplayedMonthDate = DateTime(date.year, date.month);
- widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate);
- }
- });
- }
- void _handleYearChanged(DateTime value) {
- _vibrate();
- if (value.isBefore(widget.firstDate)) {
- value = widget.firstDate;
- } else if (value.isAfter(widget.lastDate)) {
- value = widget.lastDate;
- }
- setState(() {
- _mode = DatePickerMode.day;
- _handleMonthChanged(value);
- });
- }
- void _handleDayChanged(DateTime value) {
- _vibrate();
- setState(() {
- _selectedDate = value;
- widget.onDateChanged(_selectedDate);
- });
- }
- Widget _buildPicker() {
- switch (_mode) {
- case DatePickerMode.day:
- return _MonthPicker(
- key: _monthPickerKey,
- initialMonth: _currentDisplayedMonthDate,
- currentDate: widget.currentDate,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- selectedDate: _selectedDate,
- onChanged: _handleDayChanged,
- onDisplayedMonthChanged: _handleMonthChanged,
- selectableDayPredicate: widget.selectableDayPredicate,
- );
- case DatePickerMode.year:
- return Padding(
- padding: EdgeInsets.only(top: _subHeaderHeight),
- child: YearPicker(
- key: _yearPickerKey,
- currentDate: widget.currentDate,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- initialDate: _currentDisplayedMonthDate,
- selectedDate: _selectedDate,
- onChanged: _handleYearChanged,
- ),
- );
- }
- }
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasMaterial(context));
- assert(debugCheckHasMaterialLocalizations(context));
- assert(debugCheckHasDirectionality(context));
- return Stack(
- children: <Widget>[
- SizedBox(
- height: _subHeaderHeight + _maxDayPickerHeight,
- child: _buildPicker(),
- ),
- // Put the mode toggle button on top so that it won't be covered up by the _MonthPicker
- _DatePickerModeToggleButton(
- mode: _mode,
- title: _localizations.formatMonthYear(_currentDisplayedMonthDate),
- onTitlePressed: () {
- // Toggle the day/year mode.
- _handleModeChanged(_mode == DatePickerMode.day
- ? DatePickerMode.year
- : DatePickerMode.day);
- },
- ),
- ],
- );
- }
- }
- /// A button that used to toggle the [DatePickerMode] for a date picker.
- ///
- /// This appears above the calendar grid and allows the user to toggle the
- /// [DatePickerMode] to display either the calendar view or the year list.
- class _DatePickerModeToggleButton extends StatefulWidget {
- const _DatePickerModeToggleButton({
- required this.mode,
- required this.title,
- required this.onTitlePressed,
- });
- /// The current display of the calendar picker.
- final DatePickerMode mode;
- /// The text that displays the current month/year being viewed.
- final String title;
- /// The callback when the title is pressed.
- final VoidCallback onTitlePressed;
- @override
- _DatePickerModeToggleButtonState createState() =>
- _DatePickerModeToggleButtonState();
- }
- class _DatePickerModeToggleButtonState
- extends State<_DatePickerModeToggleButton>
- with SingleTickerProviderStateMixin {
- late AnimationController _controller;
- @override
- void initState() {
- super.initState();
- _controller = AnimationController(
- value: widget.mode == DatePickerMode.year ? 0.5 : 0,
- upperBound: 0.5,
- duration: const Duration(milliseconds: 200),
- vsync: this,
- );
- }
- @override
- void didUpdateWidget(_DatePickerModeToggleButton oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (oldWidget.mode == widget.mode) {
- return;
- }
- if (widget.mode == DatePickerMode.year) {
- _controller.forward();
- } else {
- _controller.reverse();
- }
- }
- @override
- Widget build(BuildContext context) {
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
- final TextTheme textTheme = Theme.of(context).textTheme;
- final Color controlColor = colorScheme.onSurface.withOpacity(0.60);
- return Container(
- padding: EdgeInsetsDirectional.only(start: 16.s, end: 4.s),
- height: _subHeaderHeight,
- child: Row(
- children: <Widget>[
- Flexible(
- child: Semantics(
- label: MaterialLocalizations.of(context).selectYearSemanticsLabel,
- excludeSemantics: true,
- button: true,
- child: SizedBox(
- height: _subHeaderHeight,
- child: InkWell(
- onTap: widget.onTitlePressed,
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 8.s),
- child: Row(
- children: <Widget>[
- Flexible(
- child: Text(
- widget.title,
- overflow: TextOverflow.ellipsis,
- style: textTheme.titleSmall?.copyWith(
- color: controlColor,
- ),
- ),
- ),
- RotationTransition(
- turns: _controller,
- child: Icon(
- Icons.arrow_drop_down,
- color: controlColor,
- size: 24.s,
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- ),
- ),
- if (widget.mode == DatePickerMode.day)
- // Give space for the prev/next month buttons that are underneath this row
- SizedBox(width: _monthNavButtonsWidth),
- ],
- ),
- );
- }
- @override
- void dispose() {
- _controller.dispose();
- super.dispose();
- }
- }
- class _MonthPicker extends StatefulWidget {
- /// Creates a month picker.
- _MonthPicker({
- super.key,
- required this.initialMonth,
- required this.currentDate,
- required this.firstDate,
- required this.lastDate,
- required this.selectedDate,
- required this.onChanged,
- required this.onDisplayedMonthChanged,
- this.selectableDayPredicate,
- }) : assert(selectedDate != null),
- assert(currentDate != null),
- assert(onChanged != null),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(!firstDate.isAfter(lastDate)),
- assert(!selectedDate.isBefore(firstDate)),
- assert(!selectedDate.isAfter(lastDate));
- /// The initial month to display.
- final DateTime initialMonth;
- /// The current date.
- ///
- /// This date is subtly highlighted in the picker.
- final DateTime currentDate;
- /// The earliest date the user is permitted to pick.
- ///
- /// This date must be on or before the [lastDate].
- final DateTime firstDate;
- /// The latest date the user is permitted to pick.
- ///
- /// This date must be on or after the [firstDate].
- final DateTime lastDate;
- /// The currently selected date.
- ///
- /// This date is highlighted in the picker.
- final DateTime selectedDate;
- /// Called when the user picks a day.
- final ValueChanged<DateTime> onChanged;
- /// Called when the user navigates to a new month.
- final ValueChanged<DateTime> onDisplayedMonthChanged;
- /// Optional user supplied predicate function to customize selectable days.
- final SelectableDayPredicate? selectableDayPredicate;
- @override
- _MonthPickerState createState() => _MonthPickerState();
- }
- class _MonthPickerState extends State<_MonthPicker> {
- final GlobalKey _pageViewKey = GlobalKey();
- late DateTime _currentMonth;
- late PageController _pageController;
- late MaterialLocalizations _localizations;
- late TextDirection _textDirection;
- Map<ShortcutActivator, Intent>? _shortcutMap;
- Map<Type, Action<Intent>>? _actionMap;
- late FocusNode _dayGridFocus;
- DateTime? _focusedDay;
- @override
- void initState() {
- super.initState();
- _currentMonth = widget.initialMonth;
- _pageController = PageController(
- initialPage: DateUtils.monthDelta(widget.firstDate, _currentMonth));
- _shortcutMap = const <ShortcutActivator, Intent>{
- SingleActivator(LogicalKeyboardKey.arrowLeft):
- DirectionalFocusIntent(TraversalDirection.left),
- SingleActivator(LogicalKeyboardKey.arrowRight):
- DirectionalFocusIntent(TraversalDirection.right),
- SingleActivator(LogicalKeyboardKey.arrowDown):
- DirectionalFocusIntent(TraversalDirection.down),
- SingleActivator(LogicalKeyboardKey.arrowUp):
- DirectionalFocusIntent(TraversalDirection.up),
- };
- _actionMap = <Type, Action<Intent>>{
- NextFocusIntent:
- CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
- PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(
- onInvoke: _handleGridPreviousFocus),
- DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(
- onInvoke: _handleDirectionFocus),
- };
- _dayGridFocus = FocusNode(debugLabel: 'Day Grid');
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- _localizations = MaterialLocalizations.of(context);
- _textDirection = Directionality.of(context);
- }
- @override
- void didUpdateWidget(_MonthPicker oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.initialMonth != oldWidget.initialMonth &&
- widget.initialMonth != _currentMonth) {
- // We can't interrupt this widget build with a scroll, so do it next frame
- WidgetsBinding.instance.addPostFrameCallback(
- (Duration timeStamp) => _showMonth(widget.initialMonth, jump: true),
- );
- }
- }
- @override
- void dispose() {
- _pageController.dispose();
- _dayGridFocus.dispose();
- super.dispose();
- }
- void _handleDateSelected(DateTime selectedDate) {
- _focusedDay = selectedDate;
- widget.onChanged(selectedDate);
- }
- void _handleMonthPageChanged(int monthPage) {
- setState(() {
- final DateTime monthDate =
- DateUtils.addMonthsToMonthDate(widget.firstDate, monthPage);
- if (!DateUtils.isSameMonth(_currentMonth, monthDate)) {
- _currentMonth = DateTime(monthDate.year, monthDate.month);
- widget.onDisplayedMonthChanged(_currentMonth);
- if (_focusedDay != null &&
- !DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
- // We have navigated to a new month with the grid focused, but the
- // focused day is not in this month. Choose a new one trying to keep
- // the same day of the month.
- _focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day);
- }
- SemanticsService.announce(
- _localizations.formatMonthYear(_currentMonth),
- _textDirection,
- );
- }
- });
- }
- /// Returns a focusable date for the given month.
- ///
- /// If the preferredDay is available in the month it will be returned,
- /// otherwise the first selectable day in the month will be returned. If
- /// no dates are selectable in the month, then it will return null.
- DateTime? _focusableDayForMonth(DateTime month, int preferredDay) {
- final int daysInMonth = DateUtils.getDaysInMonth(month.year, month.month);
- // Can we use the preferred day in this month?
- if (preferredDay <= daysInMonth) {
- final DateTime newFocus = DateTime(month.year, month.month, preferredDay);
- if (_isSelectable(newFocus)) {
- return newFocus;
- }
- }
- // Start at the 1st and take the first selectable date.
- for (int day = 1; day <= daysInMonth; day++) {
- final DateTime newFocus = DateTime(month.year, month.month, day);
- if (_isSelectable(newFocus)) {
- return newFocus;
- }
- }
- return null;
- }
- /// Navigate to the next month.
- void _handleNextMonth() {
- if (!_isDisplayingLastMonth) {
- _pageController.nextPage(
- duration: _monthScrollDuration,
- curve: Curves.ease,
- );
- }
- }
- /// Navigate to the previous month.
- void _handlePreviousMonth() {
- if (!_isDisplayingFirstMonth) {
- _pageController.previousPage(
- duration: _monthScrollDuration,
- curve: Curves.ease,
- );
- }
- }
- /// Navigate to the given month.
- void _showMonth(DateTime month, {bool jump = false}) {
- final int monthPage = DateUtils.monthDelta(widget.firstDate, month);
- if (jump) {
- _pageController.jumpToPage(monthPage);
- } else {
- _pageController.animateToPage(
- monthPage,
- duration: _monthScrollDuration,
- curve: Curves.ease,
- );
- }
- }
- /// True if the earliest allowable month is displayed.
- bool get _isDisplayingFirstMonth {
- return !_currentMonth.isAfter(
- DateTime(widget.firstDate.year, widget.firstDate.month),
- );
- }
- /// True if the latest allowable month is displayed.
- bool get _isDisplayingLastMonth {
- return !_currentMonth.isBefore(
- DateTime(widget.lastDate.year, widget.lastDate.month),
- );
- }
- /// Handler for when the overall day grid obtains or loses focus.
- void _handleGridFocusChange(bool focused) {
- setState(() {
- if (focused && _focusedDay == null) {
- if (DateUtils.isSameMonth(widget.selectedDate, _currentMonth)) {
- _focusedDay = widget.selectedDate;
- } else if (DateUtils.isSameMonth(widget.currentDate, _currentMonth)) {
- _focusedDay =
- _focusableDayForMonth(_currentMonth, widget.currentDate.day);
- } else {
- _focusedDay = _focusableDayForMonth(_currentMonth, 1);
- }
- }
- });
- }
- /// Move focus to the next element after the day grid.
- void _handleGridNextFocus(NextFocusIntent intent) {
- _dayGridFocus.requestFocus();
- _dayGridFocus.nextFocus();
- }
- /// Move focus to the previous element before the day grid.
- void _handleGridPreviousFocus(PreviousFocusIntent intent) {
- _dayGridFocus.requestFocus();
- _dayGridFocus.previousFocus();
- }
- /// Move the internal focus date in the direction of the given intent.
- ///
- /// This will attempt to move the focused day to the next selectable day in
- /// the given direction. If the new date is not in the current month, then
- /// the page view will be scrolled to show the new date's month.
- ///
- /// For horizontal directions, it will move forward or backward a day (depending
- /// on the current [TextDirection]). For vertical directions it will move up and
- /// down a week at a time.
- void _handleDirectionFocus(DirectionalFocusIntent intent) {
- assert(_focusedDay != null);
- setState(() {
- final DateTime? nextDate =
- _nextDateInDirection(_focusedDay!, intent.direction);
- if (nextDate != null) {
- _focusedDay = nextDate;
- if (!DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
- _showMonth(_focusedDay!);
- }
- }
- });
- }
- static const Map<TraversalDirection, int> _directionOffset =
- <TraversalDirection, int>{
- TraversalDirection.up: -DateTime.daysPerWeek,
- TraversalDirection.right: 1,
- TraversalDirection.down: DateTime.daysPerWeek,
- TraversalDirection.left: -1,
- };
- int _dayDirectionOffset(
- TraversalDirection traversalDirection, TextDirection textDirection) {
- // Swap left and right if the text direction if RTL
- if (textDirection == TextDirection.rtl) {
- if (traversalDirection == TraversalDirection.left) {
- traversalDirection = TraversalDirection.right;
- } else if (traversalDirection == TraversalDirection.right) {
- traversalDirection = TraversalDirection.left;
- }
- }
- return _directionOffset[traversalDirection]!;
- }
- DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
- final TextDirection textDirection = Directionality.of(context);
- DateTime nextDate = DateUtils.addDaysToDate(
- date, _dayDirectionOffset(direction, textDirection));
- while (!nextDate.isBefore(widget.firstDate) &&
- !nextDate.isAfter(widget.lastDate)) {
- if (_isSelectable(nextDate)) {
- return nextDate;
- }
- nextDate = DateUtils.addDaysToDate(
- nextDate, _dayDirectionOffset(direction, textDirection));
- }
- return null;
- }
- bool _isSelectable(DateTime date) {
- return widget.selectableDayPredicate == null ||
- widget.selectableDayPredicate!.call(date);
- }
- Widget _buildItems(BuildContext context, int index) {
- final DateTime month =
- DateUtils.addMonthsToMonthDate(widget.firstDate, index);
- return _DayPicker(
- key: ValueKey<DateTime>(month),
- selectedDate: widget.selectedDate,
- currentDate: widget.currentDate,
- onChanged: _handleDateSelected,
- firstDate: widget.firstDate,
- lastDate: widget.lastDate,
- displayedMonth: month,
- selectableDayPredicate: widget.selectableDayPredicate,
- );
- }
- @override
- Widget build(BuildContext context) {
- final Color controlColor =
- Theme.of(context).colorScheme.onSurface.withOpacity(0.60);
- return Semantics(
- child: Column(
- children: <Widget>[
- Container(
- padding: EdgeInsetsDirectional.only(start: 16.s, end: 4.s),
- height: _subHeaderHeight,
- child: Row(
- children: <Widget>[
- const Spacer(),
- CustomIconButton(
- icon: const Icon(
- Icons.chevron_left,
- ),
- padding: EdgeInsets.all(8.s),
- iconSize: 30.s,
- constraints: const BoxConstraints(),
- color: controlColor,
- tooltip: _isDisplayingFirstMonth
- ? null
- : _localizations.previousMonthTooltip,
- onPressed:
- _isDisplayingFirstMonth ? null : _handlePreviousMonth,
- ),
- CustomIconButton(
- icon: const Icon(
- Icons.chevron_right,
- ),
- padding: EdgeInsets.all(8.s),
- iconSize: 30.s,
- constraints: const BoxConstraints(),
- color: controlColor,
- tooltip: _isDisplayingLastMonth
- ? null
- : _localizations.nextMonthTooltip,
- onPressed: _isDisplayingLastMonth ? null : _handleNextMonth,
- ),
- ],
- ),
- ),
- Expanded(
- child: FocusableActionDetector(
- shortcuts: _shortcutMap,
- actions: _actionMap,
- focusNode: _dayGridFocus,
- onFocusChange: _handleGridFocusChange,
- child: _FocusedDate(
- date: _dayGridFocus.hasFocus ? _focusedDay : null,
- child: PageView.builder(
- key: _pageViewKey,
- controller: _pageController,
- itemBuilder: _buildItems,
- itemCount:
- DateUtils.monthDelta(widget.firstDate, widget.lastDate) +
- 1,
- onPageChanged: _handleMonthPageChanged,
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
- /// InheritedWidget indicating what the current focused date is for its children.
- ///
- /// This is used by the [_MonthPicker] to let its children [_DayPicker]s know
- /// what the currently focused date (if any) should be.
- class _FocusedDate extends InheritedWidget {
- const _FocusedDate({
- required super.child,
- this.date,
- });
- final DateTime? date;
- @override
- bool updateShouldNotify(_FocusedDate oldWidget) {
- return !DateUtils.isSameDay(date, oldWidget.date);
- }
- static DateTime? of(BuildContext context) {
- final _FocusedDate? focusedDate =
- context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
- return focusedDate?.date;
- }
- }
- /// 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.
- class _DayPicker extends StatefulWidget {
- /// Creates a day picker.
- _DayPicker({
- super.key,
- required this.currentDate,
- required this.displayedMonth,
- required this.firstDate,
- required this.lastDate,
- required this.selectedDate,
- required this.onChanged,
- this.selectableDayPredicate,
- }) : assert(currentDate != null),
- assert(displayedMonth != null),
- assert(firstDate != null),
- assert(lastDate != null),
- assert(selectedDate != null),
- assert(onChanged != null),
- assert(!firstDate.isAfter(lastDate)),
- assert(!selectedDate.isBefore(firstDate)),
- assert(!selectedDate.isAfter(lastDate));
- /// The currently selected date.
- ///
- /// This date is highlighted in the picker.
- final DateTime selectedDate;
- /// The current date at the time the picker is displayed.
- final DateTime currentDate;
- /// Called when the user picks a day.
- final ValueChanged<DateTime> onChanged;
- /// The earliest date the user is permitted to pick.
- ///
- /// This date must be on or before the [lastDate].
- final DateTime firstDate;
- /// The latest date the user is permitted to pick.
- ///
- /// This date must be on or after the [firstDate].
- final DateTime lastDate;
- /// The month whose days are displayed by this picker.
- final DateTime displayedMonth;
- /// Optional user supplied predicate function to customize selectable days.
- final SelectableDayPredicate? selectableDayPredicate;
- @override
- _DayPickerState createState() => _DayPickerState();
- }
- class _DayPickerState extends State<_DayPicker> {
- /// List of [FocusNode]s, one for each day of the month.
- late List<FocusNode> _dayFocusNodes;
- @override
- void initState() {
- super.initState();
- final int daysInMonth = DateUtils.getDaysInMonth(
- widget.displayedMonth.year, widget.displayedMonth.month);
- _dayFocusNodes = List<FocusNode>.generate(
- daysInMonth,
- (int index) =>
- FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}'),
- );
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- // Check to see if the focused date is in this month, if so focus it.
- final DateTime? focusedDate = _FocusedDate.of(context);
- if (focusedDate != null &&
- DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
- _dayFocusNodes[focusedDate.day - 1].requestFocus();
- }
- }
- @override
- void dispose() {
- for (final FocusNode node in _dayFocusNodes) {
- node.dispose();
- }
- super.dispose();
- }
- /// 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
- ///
- List<Widget> _dayHeaders(
- TextStyle? headerStyle, MaterialLocalizations localizations) {
- final List<Widget> result = <Widget>[];
- for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
- final String weekday = localizations.narrowWeekdays[i];
- result.add(ExcludeSemantics(
- child: Center(child: Text(weekday, style: headerStyle)),
- ));
- if (i == (localizations.firstDayOfWeekIndex - 1) % 7) {
- break;
- }
- }
- return result;
- }
- @override
- Widget build(BuildContext context) {
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
- final TextTheme textTheme = Theme.of(context).textTheme;
- final TextStyle? headerStyle = textTheme.bodySmall?.apply(
- color: colorScheme.onSurface.withOpacity(0.60),
- );
- final TextStyle dayStyle = textTheme.bodySmall!;
- final Color enabledDayColor = colorScheme.onSurface.withOpacity(0.87);
- final Color disabledDayColor = colorScheme.onSurface.withOpacity(0.38);
- final Color selectedDayColor = colorScheme.onPrimary;
- final Color selectedDayBackground = colorScheme.primary;
- final Color todayColor = colorScheme.primary;
- final int year = widget.displayedMonth.year;
- final int month = widget.displayedMonth.month;
- final int daysInMonth = DateUtils.getDaysInMonth(year, month);
- final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
- final List<Widget> dayItems = _dayHeaders(headerStyle, localizations);
- // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
- // a leap year.
- int day = -dayOffset;
- while (day < daysInMonth) {
- day++;
- if (day < 1) {
- dayItems.add(Container());
- } else {
- final DateTime dayToBuild = DateTime(year, month, day);
- final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
- dayToBuild.isBefore(widget.firstDate) ||
- (widget.selectableDayPredicate != null &&
- !widget.selectableDayPredicate!(dayToBuild));
- final bool isSelectedDay =
- DateUtils.isSameDay(widget.selectedDate, dayToBuild);
- final bool isToday =
- DateUtils.isSameDay(widget.currentDate, dayToBuild);
- BoxDecoration? decoration;
- Color dayColor = enabledDayColor;
- if (isSelectedDay) {
- // The selected day gets a circle background highlight, and a
- // contrasting text color.
- dayColor = selectedDayColor;
- decoration = BoxDecoration(
- color: selectedDayBackground,
- shape: BoxShape.circle,
- );
- } else if (isDisabled) {
- dayColor = disabledDayColor;
- } else if (isToday) {
- // The current day gets a different text color and a circle stroke
- // border.
- dayColor = todayColor;
- decoration = BoxDecoration(
- border: Border.all(color: todayColor, width: 1.s),
- shape: BoxShape.circle,
- );
- }
- Widget dayWidget = Container(
- decoration: decoration,
- child: Center(
- child: Text(localizations.formatDecimal(day),
- style: dayStyle.apply(color: dayColor)),
- ),
- );
- if (isDisabled) {
- dayWidget = ExcludeSemantics(
- child: dayWidget,
- );
- } else {
- dayWidget = InkResponse(
- focusNode: _dayFocusNodes[day - 1],
- onTap: () => widget.onChanged(dayToBuild),
- radius: _dayPickerRowHeight / 2 + 4.s,
- splashColor: selectedDayBackground.withOpacity(0.38),
- child: Semantics(
- // 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,
- excludeSemantics: true,
- child: dayWidget,
- ),
- );
- }
- dayItems.add(dayWidget);
- }
- }
- return Padding(
- padding: EdgeInsets.symmetric(
- horizontal: _monthPickerHorizontalPadding,
- ),
- child: GridView.custom(
- physics: const ClampingScrollPhysics(),
- gridDelegate: _dayPickerGridDelegate,
- childrenDelegate: SliverChildListDelegate(
- dayItems,
- addRepaintBoundaries: false,
- ),
- ),
- );
- }
- }
- class _DayPickerGridDelegate extends SliverGridDelegate {
- const _DayPickerGridDelegate();
- @override
- SliverGridLayout getLayout(SliverConstraints constraints) {
- const int columnCount = DateTime.daysPerWeek;
- final double tileWidth = constraints.crossAxisExtent / columnCount;
- final double tileHeight = math.min(
- _dayPickerRowHeight,
- constraints.viewportMainAxisExtent / (_maxDayPickerRowCount + 1),
- );
- return SliverGridRegularTileLayout(
- childCrossAxisExtent: tileWidth,
- childMainAxisExtent: tileHeight,
- crossAxisCount: columnCount,
- crossAxisStride: tileWidth,
- mainAxisStride: tileHeight,
- reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
- );
- }
- @override
- bool shouldRelayout(_DayPickerGridDelegate oldDelegate) => false;
- }
- const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate();
- /// A scrollable grid of years to allow picking a year.
- ///
- /// The year picker widget is rarely used directly. Instead, consider using
- /// [CustomCalendarDatePicker], or [showCustomDatePicker] which create full date pickers.
- ///
- /// See also:
- ///
- /// * [CustomCalendarDatePicker], which provides a Material Design date picker
- /// interface.
- ///
- /// * [showCustomDatePicker], which shows a dialog containing a Material Design
- /// date picker.
- ///
- class YearPicker extends StatefulWidget {
- /// Creates a year picker.
- ///
- /// The [firstDate], [lastDate], [selectedDate], and [onChanged]
- /// arguments must be non-null. The [lastDate] must be after the [firstDate].
- YearPicker({
- super.key,
- DateTime? currentDate,
- required this.firstDate,
- required this.lastDate,
- DateTime? initialDate,
- required this.selectedDate,
- required this.onChanged,
- this.dragStartBehavior = DragStartBehavior.start,
- }) : assert(firstDate != null),
- assert(lastDate != null),
- assert(selectedDate != null),
- assert(onChanged != null),
- assert(!firstDate.isAfter(lastDate)),
- currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
- initialDate = DateUtils.dateOnly(initialDate ?? selectedDate);
- /// The current date.
- ///
- /// This date is subtly highlighted in the picker.
- final DateTime currentDate;
- /// The earliest date the user is permitted to pick.
- final DateTime firstDate;
- /// The latest date the user is permitted to pick.
- final DateTime lastDate;
- /// The initial date to center the year display around.
- final DateTime initialDate;
- /// The currently selected date.
- ///
- /// This date is highlighted in the picker.
- final DateTime selectedDate;
- /// Called when the user picks a year.
- final ValueChanged<DateTime> onChanged;
- /// {@macro flutter.widgets.scrollable.dragStartBehavior}
- final DragStartBehavior dragStartBehavior;
- @override
- State<YearPicker> createState() => _YearPickerState();
- }
- class _YearPickerState extends State<YearPicker> {
- late ScrollController _scrollController;
- // The approximate number of years necessary to fill the available space.
- static const int minYears = 18;
- @override
- void initState() {
- super.initState();
- _scrollController = ScrollController(
- initialScrollOffset: _scrollOffsetForYear(widget.selectedDate));
- }
- @override
- void didUpdateWidget(YearPicker oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.selectedDate != oldWidget.selectedDate) {
- _scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate));
- }
- }
- double _scrollOffsetForYear(DateTime date) {
- final int initialYearIndex = date.year - widget.firstDate.year;
- final int initialYearRow = initialYearIndex ~/ _yearPickerColumnCount;
- // Move the offset down by 2 rows to approximately center it.
- final int centeredYearRow = initialYearRow - 2;
- return _itemCount < minYears ? 0 : centeredYearRow * _yearPickerRowHeight;
- }
- Widget _buildYearItem(BuildContext context, int index) {
- final ColorScheme colorScheme = Theme.of(context).colorScheme;
- final TextTheme textTheme = Theme.of(context).textTheme;
- // Backfill the _YearPicker with disabled years if necessary.
- final int offset = _itemCount < minYears ? (minYears - _itemCount) ~/ 2 : 0;
- final int year = widget.firstDate.year + index - offset;
- final bool isSelected = year == widget.selectedDate.year;
- final bool isCurrentYear = year == widget.currentDate.year;
- final bool isDisabled =
- year < widget.firstDate.year || year > widget.lastDate.year;
- double decorationHeight = 36.s;
- double decorationWidth = 72.s;
- final Color textColor;
- if (isSelected) {
- textColor = colorScheme.onPrimary;
- } else if (isDisabled) {
- textColor = colorScheme.onSurface.withOpacity(0.38);
- } else if (isCurrentYear) {
- textColor = colorScheme.primary;
- } else {
- textColor = colorScheme.onSurface.withOpacity(0.87);
- }
- final TextStyle? itemStyle = textTheme.bodyLarge?.apply(color: textColor);
- BoxDecoration? decoration;
- if (isSelected) {
- decoration = BoxDecoration(
- color: colorScheme.primary,
- borderRadius: BorderRadius.circular(decorationHeight / 2),
- );
- } else if (isCurrentYear && !isDisabled) {
- decoration = BoxDecoration(
- border: Border.all(color: colorScheme.primary, width: 1.s),
- borderRadius: BorderRadius.circular(decorationHeight / 2),
- );
- }
- Widget yearItem = Center(
- child: Container(
- decoration: decoration,
- height: decorationHeight,
- width: decorationWidth,
- child: Center(
- child: Semantics(
- selected: isSelected,
- button: true,
- child: Text(year.toString(), style: itemStyle),
- ),
- ),
- ),
- );
- if (isDisabled) {
- yearItem = ExcludeSemantics(
- child: yearItem,
- );
- } else {
- yearItem = InkWell(
- key: ValueKey<int>(year),
- onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month)),
- child: yearItem,
- );
- }
- return yearItem;
- }
- int get _itemCount {
- return widget.lastDate.year - widget.firstDate.year + 1;
- }
- @override
- Widget build(BuildContext context) {
- assert(debugCheckHasMaterial(context));
- return Column(
- children: <Widget>[
- const Divider(),
- Expanded(
- child: GridView.builder(
- controller: _scrollController,
- dragStartBehavior: widget.dragStartBehavior,
- gridDelegate: _yearPickerGridDelegate,
- itemBuilder: _buildYearItem,
- itemCount: math.max(_itemCount, minYears),
- padding: EdgeInsets.symmetric(horizontal: _yearPickerPadding),
- ),
- ),
- const Divider(),
- ],
- );
- }
- }
- class _YearPickerGridDelegate extends SliverGridDelegate {
- const _YearPickerGridDelegate();
- @override
- SliverGridLayout getLayout(SliverConstraints constraints) {
- final double tileWidth = (constraints.crossAxisExtent -
- (_yearPickerColumnCount - 1) * _yearPickerRowSpacing) /
- _yearPickerColumnCount;
- return SliverGridRegularTileLayout(
- childCrossAxisExtent: tileWidth,
- childMainAxisExtent: _yearPickerRowHeight,
- crossAxisCount: _yearPickerColumnCount,
- crossAxisStride: tileWidth + _yearPickerRowSpacing,
- mainAxisStride: _yearPickerRowHeight,
- reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
- );
- }
- @override
- bool shouldRelayout(_YearPickerGridDelegate oldDelegate) => false;
- }
- const _YearPickerGridDelegate _yearPickerGridDelegate =
- _YearPickerGridDelegate();
|