123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578 |
- import 'dart:async';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter_localizations/flutter_localizations.dart';
- import 'package:intl/intl.dart' as intl;
- import 'date_picker_keys.dart';
- import 'day_type.dart';
- import 'i_selectable_picker.dart';
- import 'month_picker_selection.dart';
- import 'semantic_sorting.dart';
- import 'styles/date_picker_styles.dart';
- import 'styles/layout_settings.dart';
- import 'utils.dart';
- const Locale _defaultLocale = Locale('en', 'US');
- /// Month picker widget.
- class WMonthPicker<T extends Object> extends StatefulWidget {
- WMonthPicker({
- Key? key,
- required this.selectionLogic,
- required this.selection,
- required this.onChanged,
- required this.firstDate,
- required this.lastDate,
- this.datePickerLayoutSettings = const DatePickerLayoutSettings(),
- this.datePickerKeys,
- required this.datePickerStyles,
- }) : assert(!firstDate!.isAfter(lastDate!)),
- assert(
- selection!.isEmpty || !selection.isBefore(firstDate!),
- 'Selection must not be before first date. '
- 'Earliest selection is: ${selection.earliest}. '
- 'First date is: $firstDate'),
- assert(
- selection!.isEmpty || !selection.isAfter(lastDate!),
- 'Selection must not be after last date. '
- 'Latest selection is: ${selection.latest}. '
- 'First date is: $lastDate'),
- super(key: key);
- /// Creates a month picker where only one single month can be selected.
- ///
- /// See also:
- /// * [WMonthPicker.multi] - month picker where many single months
- /// can be selected.
- static WMonthPicker<DateTime> single(
- {Key? key,
- required DateTime selectedDate,
- required ValueChanged<DateTime> onChanged,
- required DateTime firstDate,
- required DateTime lastDate,
- DatePickerLayoutSettings? datePickerLayoutSettings =
- const DatePickerLayoutSettings(),
- DatePickerStyles? datePickerStyles,
- DatePickerKeys? datePickerKeys,
- SelectableDayPredicate? selectableDayPredicate,
- ValueChanged<DateTime>? onMonthChanged}) {
- assert(!firstDate.isAfter(lastDate));
- assert(!lastDate.isBefore(firstDate));
- assert(!selectedDate.isBefore(firstDate));
- assert(!selectedDate.isAfter(lastDate));
- final selection = MonthPickerSingleSelection(selectedDate);
- final selectionLogic = MonthSelectable(selectedDate, firstDate, lastDate,
- selectableDayPredicate: selectableDayPredicate);
- return WMonthPicker<DateTime>(
- onChanged: onChanged,
- firstDate: firstDate,
- lastDate: lastDate,
- selectionLogic: selectionLogic,
- selection: selection,
- datePickerKeys: datePickerKeys,
- datePickerStyles: datePickerStyles ?? DatePickerRangeStyles(),
- datePickerLayoutSettings: datePickerLayoutSettings,
- );
- }
- /// Creates a month picker where many single months can be selected.
- ///
- /// See also:
- /// * [WMonthPicker.single] - month picker where only one single month
- /// can be selected.
- static WMonthPicker<List<DateTime>> multi(
- {Key? key,
- required List<DateTime> selectedDates,
- required ValueChanged<List<DateTime>> onChanged,
- required DateTime firstDate,
- required DateTime lastDate,
- DatePickerLayoutSettings? datePickerLayoutSettings =
- const DatePickerLayoutSettings(),
- DatePickerStyles? datePickerStyles,
- DatePickerKeys? datePickerKeys,
- SelectableDayPredicate? selectableDayPredicate,
- ValueChanged<DateTime>? onMonthChanged}) {
- assert(!firstDate.isAfter(lastDate));
- assert(!lastDate.isBefore(firstDate));
- final selection = MonthPickerMultiSelection(selectedDates);
- final selectionLogic = MonthMultiSelectable(
- selectedDates, firstDate, lastDate,
- selectableDayPredicate: selectableDayPredicate);
- return WMonthPicker<List<DateTime>>(
- onChanged: onChanged,
- firstDate: firstDate,
- lastDate: lastDate,
- selectionLogic: selectionLogic,
- selection: selection,
- datePickerKeys: datePickerKeys,
- datePickerStyles: datePickerStyles,
- datePickerLayoutSettings: datePickerLayoutSettings,
- );
- }
- /// The currently selected date or dates.
- ///
- /// This date or dates are highlighted in the picker.
- final MonthPickerSelection? selection;
- /// Called when the user picks a month.
- final ValueChanged<T>? onChanged;
- /// The earliest date the user is permitted to pick.
- final DateTime? firstDate;
- /// The latest date the user is permitted to pick.
- final DateTime? lastDate;
- /// Layout settings what can be customized by user
- final DatePickerLayoutSettings? datePickerLayoutSettings;
- /// Some keys useful for integration tests
- final DatePickerKeys? datePickerKeys;
- /// Styles what can be customized by user
- final DatePickerStyles? datePickerStyles;
- /// Logic to handle user's selections.
- final ISelectablePicker<T>? selectionLogic;
- @override
- State<StatefulWidget> createState() => _WMonthPickerState<T>();
- }
- class _WMonthPickerState<T extends Object> extends State<WMonthPicker<T>> {
- PageController _monthPickerController = PageController();
- Locale locale = _defaultLocale;
- MaterialLocalizations localizations = _defaultLocalizations;
- TextDirection textDirection = TextDirection.ltr;
- DateTime? _todayDate = DateTime.now();
- DateTime? _previousYearDate = DateTime(DateTime.now().year - 1);
- DateTime? _nextYearDate = DateTime(DateTime.now().year + 1);
- DateTime? _currentDisplayedYearDate = DateTime.now();
- Timer? _timer;
- StreamSubscription<T>? _changesSubscription;
- /// True if the earliest allowable year is displayed.
- bool get _isDisplayingFirstYear =>
- !_currentDisplayedYearDate!.isAfter(DateTime(widget.firstDate!.year));
- /// True if the latest allowable year is displayed.
- bool get _isDisplayingLastYear =>
- !_currentDisplayedYearDate!.isBefore(DateTime(widget.lastDate!.year));
- @override
- void initState() {
- super.initState();
- _initWidgetData();
- _updateCurrentDate();
- }
- @override
- void didUpdateWidget(WMonthPicker<T> oldWidget) {
- super.didUpdateWidget(oldWidget);
- if (widget.selection != oldWidget.selection ||
- widget.selectionLogic != oldWidget.selectionLogic) {
- _initWidgetData();
- }
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- try {
- locale = Localizations.localeOf(context);
- MaterialLocalizations curLocalizations =
- Localizations.of<MaterialLocalizations>(
- context, MaterialLocalizations)!;
- // ignore: unnecessary_null_comparison
- if (curLocalizations != null && localizations != curLocalizations) {
- localizations = curLocalizations;
- }
- textDirection = Directionality.of(context);
- // No MaterialLocalizations or Directionality or Locale was found
- // and ".of" method throws error
- // trying to cast null to MaterialLocalizations.
- } on TypeError catch (_) {}
- }
- @override
- Widget build(BuildContext context) {
- int yearsCount =
- DatePickerUtils.yearDelta(widget.firstDate!, widget.lastDate!) + 1;
- return SizedBox(
- width: widget.datePickerLayoutSettings!.monthPickerPortraitWidth,
- height: widget.datePickerLayoutSettings!.maxDayPickerHeight,
- child: Stack(
- children: <Widget>[
- Semantics(
- sortKey: YearPickerSortKey.calendar,
- child: PageView.builder(
- // key: ValueKey<DateTime>(widget.selection),
- controller: _monthPickerController,
- scrollDirection: Axis.horizontal,
- itemCount: yearsCount,
- itemBuilder: _buildItems,
- onPageChanged: _handleYearPageChanged,
- ),
- ),
- PositionedDirectional(
- top: 0.0,
- start: 8.0,
- child: Semantics(
- sortKey: YearPickerSortKey.previousYear,
- child: IconButton(
- key: widget.datePickerKeys?.previousPageIconKey,
- icon: widget.datePickerStyles?.prevIcon ??
- Icon(
- Icons.chevron_left,
- ),
- tooltip: _isDisplayingFirstYear
- ? null
- : '${localizations.formatYear(_previousYearDate!)}',
- onPressed: _isDisplayingFirstYear ? null : _handlePreviousYear,
- ),
- ),
- ),
- PositionedDirectional(
- top: 0.0,
- end: 8.0,
- child: Semantics(
- sortKey: YearPickerSortKey.nextYear,
- child: IconButton(
- key: widget.datePickerKeys?.nextPageIconKey,
- icon: widget.datePickerStyles?.nextIcon ??
- Icon(
- Icons.chevron_right,
- ),
- tooltip: _isDisplayingLastYear
- ? null
- : '${localizations.formatYear(_nextYearDate!)}',
- onPressed: _isDisplayingLastYear ? null : _handleNextYear,
- ),
- ),
- ),
- ],
- ),
- );
- }
- @override
- void dispose() {
- _timer!.cancel();
- _changesSubscription!.cancel();
- super.dispose();
- }
- void _initWidgetData() {
- final initiallyShowDate =
- widget.selection!.isEmpty ? DateTime.now() : widget.selection!.earliest;
- // Initially display the pre-selected date.
- final int yearPage =
- DatePickerUtils.yearDelta(widget.firstDate!, initiallyShowDate);
- if (_changesSubscription != null) {
- _changesSubscription!.cancel();
- }
- _changesSubscription = widget.selectionLogic!.onUpdate!
- .listen((newSelectedDate) => widget.onChanged!(newSelectedDate))
- ..onError((e) => print(e.toString()));
- _monthPickerController.dispose();
- _monthPickerController = PageController(initialPage: yearPage);
- _handleYearPageChanged(yearPage);
- }
- void _updateCurrentDate() {
- _todayDate = DateTime.now();
- final DateTime tomorrow =
- DateTime(_todayDate!.year, _todayDate!.month, _todayDate!.day + 1);
- Duration timeUntilTomorrow = tomorrow.difference(_todayDate!);
- timeUntilTomorrow +=
- const Duration(seconds: 1); // so we don't miss it by rounding
- if (_timer != null) {
- _timer!.cancel();
- }
- _timer = Timer(timeUntilTomorrow, () {
- setState(_updateCurrentDate);
- });
- }
- /// Add years to a year truncated date.
- DateTime _addYearsToYearDate(DateTime yearDate, int yearsToAdd) =>
- DateTime(yearDate.year + yearsToAdd);
- Widget _buildItems(BuildContext context, int index) {
- final DateTime year = _addYearsToYearDate(widget.firstDate!, index);
- final ThemeData theme = Theme.of(context);
- DatePickerStyles? styles = widget.datePickerStyles ?? null;
- styles = styles?.fulfillWithTheme(theme);
- return _WMonthPicker<T>(
- key: ValueKey<DateTime>(year),
- currentDate: _todayDate!,
- onChanged: widget.onChanged!,
- firstDate: widget.firstDate!,
- lastDate: widget.lastDate!,
- datePickerLayoutSettings: widget.datePickerLayoutSettings!,
- displayedYear: year,
- selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
- datePickerStyles: styles,
- locale: locale,
- localizations: localizations,
- selectionLogic: widget.selectionLogic!,
- );
- }
- void _handleNextYear() {
- if (!_isDisplayingLastYear) {
- String yearStr = localizations.formatYear(_nextYearDate!);
- SemanticsService.announce(yearStr, textDirection);
- _monthPickerController.nextPage(
- duration: widget.datePickerLayoutSettings!.pagesScrollDuration,
- curve: Curves.ease);
- }
- }
- void _handlePreviousYear() {
- if (!_isDisplayingFirstYear) {
- String yearStr = localizations.formatYear(_previousYearDate!);
- SemanticsService.announce(yearStr, textDirection);
- _monthPickerController.previousPage(
- duration: widget.datePickerLayoutSettings!.pagesScrollDuration,
- curve: Curves.ease);
- }
- }
- void _handleYearPageChanged(int yearPage) {
- setState(() {
- _previousYearDate = _addYearsToYearDate(widget.firstDate!, yearPage - 1);
- _currentDisplayedYearDate =
- _addYearsToYearDate(widget.firstDate!, yearPage);
- _nextYearDate = _addYearsToYearDate(widget.firstDate!, yearPage + 1);
- });
- }
- static MaterialLocalizations get _defaultLocalizations =>
- MaterialLocalizationEn(
- twoDigitZeroPaddedFormat:
- intl.NumberFormat('00', _defaultLocale.toString()),
- fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
- longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
- shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
- decimalFormat:
- intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
- shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
- mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
- compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
- yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
- );
- }
- class _WMonthPicker<T> extends StatelessWidget {
- /// The month whose days are displayed by this picker.
- final DateTime? displayedYear;
- /// 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 current date at the time the picker is displayed.
- final DateTime? currentDate;
- /// Layout settings what can be customized by user
- final DatePickerLayoutSettings? datePickerLayoutSettings;
- /// Called when the user picks a day.
- final ValueChanged<T>? onChanged;
- /// Key fo selected month (useful for integration tests)
- final Key? selectedPeriodKey;
- /// Styles what can be customized by user
- final DatePickerStyles? datePickerStyles;
- final MaterialLocalizations? localizations;
- final ISelectablePicker<T>? selectionLogic;
- final Locale? locale;
- _WMonthPicker(
- {required this.displayedYear,
- required this.firstDate,
- required this.lastDate,
- required this.currentDate,
- required this.onChanged,
- required this.datePickerLayoutSettings,
- required this.datePickerStyles,
- required this.selectionLogic,
- required this.localizations,
- required this.locale,
- this.selectedPeriodKey,
- Key? key})
- : assert(!firstDate!.isAfter(lastDate!)),
- super(key: key);
- @override
- Widget build(BuildContext context) {
- final int monthsInYear = 12;
- final int year = displayedYear!.year;
- final int day = 1;
- final List<Widget> labels = <Widget>[];
- for (int month = 1; month <= monthsInYear; month += 1) {
- DateTime monthToBuild = DateTime(year, month, day);
- DayType? monthType = selectionLogic!.getDayType(monthToBuild);
- Widget monthWidget = _MonthCell(
- monthToBuild: monthToBuild,
- currentDate: currentDate!,
- selectionLogic: selectionLogic,
- datePickerStyles: datePickerStyles,
- localizations: localizations,
- locale: locale,
- );
- if (monthType != DayType.disabled) {
- monthWidget = GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTap: () {
- DatePickerUtils.sameMonth(firstDate!, monthToBuild)
- ? selectionLogic!.onDayTapped(firstDate!)
- : selectionLogic!.onDayTapped(monthToBuild);
- },
- child: monthWidget,
- );
- }
- labels.add(monthWidget);
- }
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Column(
- children: <Widget>[
- Container(
- height: datePickerLayoutSettings!.dayPickerRowHeight,
- child: Center(
- child: ExcludeSemantics(
- child: Text(
- localizations!.formatYear(displayedYear!),
- key: selectedPeriodKey,
- style: datePickerStyles!.displayedPeriodTitle,
- ),
- ),
- ),
- ),
- Flexible(
- child: GridView.count(
- physics: datePickerLayoutSettings!.scrollPhysics,
- crossAxisCount: 4,
- children: labels,
- ),
- ),
- ],
- ),
- );
- }
- }
- class _MonthCell<T> extends StatelessWidget {
- /// Styles what can be customized by user
- final DatePickerStyles? datePickerStyles;
- final Locale? locale;
- final MaterialLocalizations? localizations;
- final ISelectablePicker<T>? selectionLogic;
- final DateTime? monthToBuild;
- /// The current date at the time the picker is displayed.
- final DateTime currentDate;
- const _MonthCell({
- required this.monthToBuild,
- required this.currentDate,
- required this.selectionLogic,
- required this.datePickerStyles,
- required this.locale,
- required this.localizations,
- Key? key,
- }) : super(key: key);
- @override
- Widget build(BuildContext context) {
- DayType? monthType = selectionLogic!.getDayType(monthToBuild!)!;
- BoxDecoration? decoration;
- TextStyle? itemStyle;
- if (monthType != DayType.disabled && monthType != DayType.notSelected) {
- itemStyle = datePickerStyles!.selectedDateStyle;
- decoration = datePickerStyles!.selectedSingleDateDecoration;
- } else if (monthType == DayType.disabled) {
- itemStyle = datePickerStyles!.disabledDateStyle;
- } else if (DatePickerUtils.sameMonth(currentDate, monthToBuild!)) {
- itemStyle = datePickerStyles!.currentDateStyle;
- } else {
- itemStyle = datePickerStyles!.defaultDateTextStyle;
- }
- String semanticLabel =
- '${localizations!.formatDecimal(monthToBuild!.month)}, '
- '${localizations!.formatFullDate(monthToBuild!)}';
- bool isSelectedMonth =
- monthType != DayType.disabled && monthType != DayType.notSelected;
- String monthStr = _getMonthStr(monthToBuild!);
- Widget monthWidget = Container(
- decoration: decoration,
- child: Center(
- 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: semanticLabel,
- selected: isSelectedMonth,
- child: ExcludeSemantics(
- child: Text(monthStr, style: itemStyle),
- ),
- ),
- ),
- );
- return monthWidget;
- }
- // Returns only month made with intl.DateFormat.MMM() for current [locale].
- // We can'r use [localizations] here because MaterialLocalizations doesn't
- // provide short month string.
- String _getMonthStr(DateTime date) {
- String month = intl.DateFormat.MMM(locale.toString()).format(date);
- return month;
- }
- }
|