forked from firka/flutter
Added calendar delegate to support custom calendar systems (#161874)
Added `CalendarDelegate` class that supports plugging in custom calendar logics other than Gregorian Calendar System. Here is an example implementation for Nepali(Bikram Sambat) Calendar System: https://github.com/sarbagyastha/nepali_date_picker/blob/m3/lib/src/nepali_calendar_delegate.dart Demo using the `NepaliDatePickerDelegate`: https://date.sarbagyastha.com.np/ Fixes https://github.com/flutter/flutter/issues/77531, https://github.com/flutter/flutter/issues/161873 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
83781ae65c
commit
6d6d7914f9
@@ -0,0 +1,148 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
/// Flutter code sample demonstrating how to use a custom [CalendarDelegate]
|
||||
/// with [CalendarDatePicker] to implement a hypothetical calendar system
|
||||
/// where even-numbered months have 21 days, odd-numbered months have 28 days,
|
||||
/// and every month starts on a Monday.
|
||||
|
||||
void main() => runApp(const CalendarDatePickerApp());
|
||||
|
||||
class CalendarDatePickerApp extends StatelessWidget {
|
||||
const CalendarDatePickerApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(home: CalendarDatePickerExample());
|
||||
}
|
||||
}
|
||||
|
||||
class CalendarDatePickerExample extends StatefulWidget {
|
||||
const CalendarDatePickerExample({super.key});
|
||||
|
||||
@override
|
||||
State<CalendarDatePickerExample> createState() => _CalendarDatePickerExampleState();
|
||||
}
|
||||
|
||||
class _CalendarDatePickerExampleState extends State<CalendarDatePickerExample> {
|
||||
DateTime? selectedDate;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Custom Calendar')),
|
||||
body: Column(
|
||||
spacing: 16,
|
||||
children: <Widget>[
|
||||
CalendarDatePicker(
|
||||
initialDate: DateTime(2025, 2, 8),
|
||||
firstDate: DateTime(2025),
|
||||
lastDate: DateTime(2026),
|
||||
onDateChanged: (DateTime pickedDate) {
|
||||
setState(() {
|
||||
selectedDate = pickedDate;
|
||||
});
|
||||
},
|
||||
calendarDelegate: const CustomCalendarDelegate(),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Text(
|
||||
selectedDate != null
|
||||
? '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}'
|
||||
: 'No date selected',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom calendar system where even-numbered months have 21 days,
|
||||
/// odd-numbered months have 28 days, and every month starts on a Monday.
|
||||
///
|
||||
/// This hypothetical calendar follows a fixed structure:
|
||||
/// - **Even-numbered months (2, 4, 6, etc.)** always have **21 days**.
|
||||
/// - **Odd-numbered months (1, 3, 5, etc.)** always have **28 days**.
|
||||
/// - **The first day of every month is always a Monday**, ensuring a consistent weekly alignment.
|
||||
class CustomCalendarDelegate extends CalendarDelegate<DateTime> {
|
||||
const CustomCalendarDelegate();
|
||||
|
||||
@override
|
||||
int getDaysInMonth(int year, int month) {
|
||||
return month.isEven ? 21 : 28;
|
||||
}
|
||||
|
||||
@override
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// All the implementations below are based on the Gregorian calendar system.
|
||||
|
||||
@override
|
||||
DateTime now() => DateTime.now();
|
||||
|
||||
@override
|
||||
DateTime dateOnly(DateTime date) => DateUtils.dateOnly(date);
|
||||
|
||||
@override
|
||||
int monthDelta(DateTime startDate, DateTime endDate) => DateUtils.monthDelta(startDate, endDate);
|
||||
|
||||
@override
|
||||
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
|
||||
return DateUtils.addMonthsToMonthDate(monthDate, monthsToAdd);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime addDaysToDate(DateTime date, int days) => DateUtils.addDaysToDate(date, days);
|
||||
|
||||
@override
|
||||
DateTime getMonth(int year, int month) => DateTime(year, month);
|
||||
|
||||
@override
|
||||
DateTime getDay(int year, int month, int day) => DateTime(year, month, day);
|
||||
|
||||
@override
|
||||
String formatMonthYear(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatMonthYear(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatMediumDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatMediumDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatShortMonthDay(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatShortMonthDay(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatShortDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatShortDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatFullDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatFullDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatCompactDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatCompactDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) {
|
||||
return localizations.parseCompactDate(inputString);
|
||||
}
|
||||
|
||||
@override
|
||||
String dateHelpText(MaterialLocalizations localizations) {
|
||||
return localizations.dateHelpText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter_api_samples/material/date_picker/custom_calendar_date_picker.0.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
Text getLastDayText(WidgetTester tester) {
|
||||
final Finder dayFinder = find.descendant(of: find.byType(Ink), matching: find.byType(Text));
|
||||
return tester.widget(dayFinder.last);
|
||||
}
|
||||
|
||||
testWidgets('Days are based on the calendar delegate', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.CalendarDatePickerApp());
|
||||
|
||||
final Finder nextMonthButton = find.byIcon(Icons.chevron_right);
|
||||
|
||||
Text lastDayText = getLastDayText(tester);
|
||||
expect(find.text('February 2025'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('21'));
|
||||
|
||||
await tester.tap(nextMonthButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
lastDayText = getLastDayText(tester);
|
||||
expect(find.text('March 2025'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('28'));
|
||||
|
||||
await tester.tap(nextMonthButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
lastDayText = getLastDayText(tester);
|
||||
expect(find.text('April 2025'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('21'));
|
||||
|
||||
await tester.tap(nextMonthButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
lastDayText = getLastDayText(tester);
|
||||
expect(lastDayText.data, equals('28'));
|
||||
});
|
||||
}
|
||||
@@ -107,6 +107,13 @@ class CalendarDatePicker extends StatefulWidget {
|
||||
///
|
||||
/// If [selectableDayPredicate] and [initialDate] are both non-null,
|
||||
/// [selectableDayPredicate] must return `true` for the [initialDate].
|
||||
///
|
||||
/// {@template flutter.material.calendar_date_picker.calendarDelegate}
|
||||
/// The [calendarDelegate] controls date interpretation, formatting, and
|
||||
/// navigation within the picker. By providing a custom implementation,
|
||||
/// you can support alternative calendar systems such as Nepali, Hijri,
|
||||
/// Buddhist, and more. Defaults to [GregorianCalendarDelegate].
|
||||
/// {@endtemplate}
|
||||
CalendarDatePicker({
|
||||
super.key,
|
||||
required DateTime? initialDate,
|
||||
@@ -117,10 +124,11 @@ class CalendarDatePicker extends StatefulWidget {
|
||||
this.onDisplayedMonthChanged,
|
||||
this.initialCalendarMode = DatePickerMode.day,
|
||||
this.selectableDayPredicate,
|
||||
}) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate),
|
||||
firstDate = DateUtils.dateOnly(firstDate),
|
||||
lastDate = DateUtils.dateOnly(lastDate),
|
||||
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
|
||||
this.calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) : initialDate = initialDate == null ? null : calendarDelegate.dateOnly(initialDate),
|
||||
firstDate = calendarDelegate.dateOnly(firstDate),
|
||||
lastDate = calendarDelegate.dateOnly(lastDate),
|
||||
currentDate = calendarDelegate.dateOnly(currentDate ?? calendarDelegate.now()) {
|
||||
assert(
|
||||
!this.lastDate.isBefore(this.firstDate),
|
||||
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
|
||||
@@ -175,6 +183,9 @@ class CalendarDatePicker extends StatefulWidget {
|
||||
/// Function to provide full control over which dates in the calendar can be selected.
|
||||
final SelectableDayPredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<CalendarDatePicker> createState() => _CalendarDatePickerState();
|
||||
}
|
||||
@@ -194,7 +205,10 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
super.initState();
|
||||
_mode = widget.initialCalendarMode;
|
||||
final DateTime currentDisplayedDate = widget.initialDate ?? widget.currentDate;
|
||||
_currentDisplayedMonthDate = DateTime(currentDisplayedDate.year, currentDisplayedDate.month);
|
||||
_currentDisplayedMonthDate = widget.calendarDelegate.getMonth(
|
||||
currentDisplayedDate.year,
|
||||
currentDisplayedDate.month,
|
||||
);
|
||||
if (widget.initialDate != null) {
|
||||
_selectedDate = widget.initialDate;
|
||||
}
|
||||
@@ -211,7 +225,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
if (!_announcedInitialDate && widget.initialDate != null) {
|
||||
assert(_selectedDate != null);
|
||||
_announcedInitialDate = true;
|
||||
final bool isToday = DateUtils.isSameDay(widget.currentDate, _selectedDate);
|
||||
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
|
||||
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
|
||||
SemanticsService.announce(
|
||||
'${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix',
|
||||
@@ -239,8 +253,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
_mode = mode;
|
||||
if (_selectedDate case final DateTime selected) {
|
||||
final String message = switch (mode) {
|
||||
DatePickerMode.day => _localizations.formatMonthYear(selected),
|
||||
DatePickerMode.year => _localizations.formatYear(selected),
|
||||
DatePickerMode.day => widget.calendarDelegate.formatMonthYear(selected, _localizations),
|
||||
DatePickerMode.year => widget.calendarDelegate.formatYear(selected.year, _localizations),
|
||||
};
|
||||
SemanticsService.announce(message, _textDirection);
|
||||
}
|
||||
@@ -251,7 +265,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
setState(() {
|
||||
if (_currentDisplayedMonthDate.year != date.year ||
|
||||
_currentDisplayedMonthDate.month != date.month) {
|
||||
_currentDisplayedMonthDate = DateTime(date.year, date.month);
|
||||
_currentDisplayedMonthDate = widget.calendarDelegate.getMonth(date.year, date.month);
|
||||
widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate);
|
||||
}
|
||||
});
|
||||
@@ -260,9 +274,9 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
void _handleYearChanged(DateTime value) {
|
||||
_vibrate();
|
||||
|
||||
final int daysInMonth = DateUtils.getDaysInMonth(value.year, value.month);
|
||||
final int daysInMonth = widget.calendarDelegate.getDaysInMonth(value.year, value.month);
|
||||
final int preferredDay = math.min(_selectedDate?.day ?? 1, daysInMonth);
|
||||
value = value.copyWith(day: preferredDay);
|
||||
value = widget.calendarDelegate.getDay(value.year, value.month, preferredDay);
|
||||
|
||||
if (value.isBefore(widget.firstDate)) {
|
||||
value = widget.firstDate;
|
||||
@@ -290,10 +304,10 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
final bool isToday = DateUtils.isSameDay(widget.currentDate, _selectedDate);
|
||||
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, _selectedDate);
|
||||
final String semanticLabelSuffix = isToday ? ', ${_localizations.currentDateLabel}' : '';
|
||||
SemanticsService.announce(
|
||||
'${_localizations.selectedDateLabel} ${_localizations.formatFullDate(_selectedDate!)}$semanticLabelSuffix',
|
||||
'${_localizations.selectedDateLabel} ${widget.calendarDelegate.formatFullDate(_selectedDate!, _localizations)}$semanticLabelSuffix',
|
||||
_textDirection,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
@@ -313,6 +327,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
case DatePickerMode.day:
|
||||
return _MonthPicker(
|
||||
key: _monthPickerKey,
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
initialMonth: _currentDisplayedMonthDate,
|
||||
currentDate: widget.currentDate,
|
||||
firstDate: widget.firstDate,
|
||||
@@ -327,6 +342,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
padding: const EdgeInsets.only(top: _subHeaderHeight),
|
||||
child: YearPicker(
|
||||
key: _yearPickerKey,
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
currentDate: widget.currentDate,
|
||||
firstDate: widget.firstDate,
|
||||
lastDate: widget.lastDate,
|
||||
@@ -363,7 +379,10 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||
maxScaleFactor: _kModeToggleButtonMaxScaleFactor,
|
||||
child: _DatePickerModeToggleButton(
|
||||
mode: _mode,
|
||||
title: _localizations.formatMonthYear(_currentDisplayedMonthDate),
|
||||
title: widget.calendarDelegate.formatMonthYear(
|
||||
_currentDisplayedMonthDate,
|
||||
_localizations,
|
||||
),
|
||||
onTitlePressed:
|
||||
() => _handleModeChanged(switch (_mode) {
|
||||
DatePickerMode.day => DatePickerMode.year,
|
||||
@@ -499,6 +518,7 @@ class _MonthPicker extends StatefulWidget {
|
||||
required this.selectedDate,
|
||||
required this.onChanged,
|
||||
required this.onDisplayedMonthChanged,
|
||||
required this.calendarDelegate,
|
||||
this.selectableDayPredicate,
|
||||
}) : assert(!firstDate.isAfter(lastDate)),
|
||||
assert(selectedDate == null || !selectedDate.isBefore(firstDate)),
|
||||
@@ -541,6 +561,9 @@ class _MonthPicker extends StatefulWidget {
|
||||
/// Optional user supplied predicate function to customize selectable days.
|
||||
final SelectableDayPredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
_MonthPickerState createState() => _MonthPickerState();
|
||||
}
|
||||
@@ -561,7 +584,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
super.initState();
|
||||
_currentMonth = widget.initialMonth;
|
||||
_pageController = PageController(
|
||||
initialPage: DateUtils.monthDelta(widget.firstDate, _currentMonth),
|
||||
initialPage: widget.calendarDelegate.monthDelta(widget.firstDate, _currentMonth),
|
||||
);
|
||||
_shortcutMap = const <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(
|
||||
@@ -606,17 +629,24 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
|
||||
void _handleMonthPageChanged(int monthPage) {
|
||||
setState(() {
|
||||
final DateTime monthDate = DateUtils.addMonthsToMonthDate(widget.firstDate, monthPage);
|
||||
if (!DateUtils.isSameMonth(_currentMonth, monthDate)) {
|
||||
_currentMonth = DateTime(monthDate.year, monthDate.month);
|
||||
final DateTime monthDate = widget.calendarDelegate.addMonthsToMonthDate(
|
||||
widget.firstDate,
|
||||
monthPage,
|
||||
);
|
||||
if (!widget.calendarDelegate.isSameMonth(_currentMonth, monthDate)) {
|
||||
_currentMonth = widget.calendarDelegate.getMonth(monthDate.year, monthDate.month);
|
||||
widget.onDisplayedMonthChanged(_currentMonth);
|
||||
if (_focusedDay != null && !DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
|
||||
if (_focusedDay != null &&
|
||||
!widget.calendarDelegate.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);
|
||||
SemanticsService.announce(
|
||||
widget.calendarDelegate.formatMonthYear(_currentMonth, _localizations),
|
||||
_textDirection,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -627,11 +657,15 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
/// 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);
|
||||
final int daysInMonth = widget.calendarDelegate.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);
|
||||
final DateTime newFocus = widget.calendarDelegate.getDay(
|
||||
month.year,
|
||||
month.month,
|
||||
preferredDay,
|
||||
);
|
||||
if (_isSelectable(newFocus)) {
|
||||
return newFocus;
|
||||
}
|
||||
@@ -639,7 +673,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
|
||||
// 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);
|
||||
final DateTime newFocus = widget.calendarDelegate.getDay(month.year, month.month, day);
|
||||
if (_isSelectable(newFocus)) {
|
||||
return newFocus;
|
||||
}
|
||||
@@ -663,7 +697,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
|
||||
/// Navigate to the given month.
|
||||
void _showMonth(DateTime month, {bool jump = false}) {
|
||||
final int monthPage = DateUtils.monthDelta(widget.firstDate, month);
|
||||
final int monthPage = widget.calendarDelegate.monthDelta(widget.firstDate, month);
|
||||
if (jump) {
|
||||
_pageController.jumpToPage(monthPage);
|
||||
} else {
|
||||
@@ -673,21 +707,25 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
|
||||
/// True if the earliest allowable month is displayed.
|
||||
bool get _isDisplayingFirstMonth {
|
||||
return !_currentMonth.isAfter(DateTime(widget.firstDate.year, widget.firstDate.month));
|
||||
return !_currentMonth.isAfter(
|
||||
widget.calendarDelegate.getMonth(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));
|
||||
return !_currentMonth.isBefore(
|
||||
widget.calendarDelegate.getMonth(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)) {
|
||||
if (widget.calendarDelegate.isSameMonth(widget.selectedDate, _currentMonth)) {
|
||||
_focusedDay = widget.selectedDate;
|
||||
} else if (DateUtils.isSameMonth(widget.currentDate, _currentMonth)) {
|
||||
} else if (widget.calendarDelegate.isSameMonth(widget.currentDate, _currentMonth)) {
|
||||
_focusedDay = _focusableDayForMonth(_currentMonth, widget.currentDate.day);
|
||||
} else {
|
||||
_focusedDay = _focusableDayForMonth(_currentMonth, 1);
|
||||
@@ -723,7 +761,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
final DateTime? nextDate = _nextDateInDirection(_focusedDay!, intent.direction);
|
||||
if (nextDate != null) {
|
||||
_focusedDay = nextDate;
|
||||
if (!DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
|
||||
if (!widget.calendarDelegate.isSameMonth(_focusedDay, _currentMonth)) {
|
||||
_showMonth(_focusedDay!);
|
||||
}
|
||||
}
|
||||
@@ -751,7 +789,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
|
||||
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
DateTime nextDate = DateUtils.addDaysToDate(
|
||||
DateTime nextDate = widget.calendarDelegate.addDaysToDate(
|
||||
date,
|
||||
_dayDirectionOffset(direction, textDirection),
|
||||
);
|
||||
@@ -759,7 +797,10 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
if (_isSelectable(nextDate)) {
|
||||
return nextDate;
|
||||
}
|
||||
nextDate = DateUtils.addDaysToDate(nextDate, _dayDirectionOffset(direction, textDirection));
|
||||
nextDate = widget.calendarDelegate.addDaysToDate(
|
||||
nextDate,
|
||||
_dayDirectionOffset(direction, textDirection),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -769,9 +810,10 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
}
|
||||
|
||||
Widget _buildItems(BuildContext context, int index) {
|
||||
final DateTime month = DateUtils.addMonthsToMonthDate(widget.firstDate, index);
|
||||
final DateTime month = widget.calendarDelegate.addMonthsToMonthDate(widget.firstDate, index);
|
||||
return _DayPicker(
|
||||
key: ValueKey<DateTime>(month),
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
selectedDate: widget.selectedDate,
|
||||
currentDate: widget.currentDate,
|
||||
onChanged: _handleDateSelected,
|
||||
@@ -821,12 +863,14 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
focusNode: _dayGridFocus,
|
||||
onFocusChange: _handleGridFocusChange,
|
||||
child: _FocusedDate(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
date: _dayGridFocus.hasFocus ? _focusedDay : null,
|
||||
child: PageView.builder(
|
||||
key: _pageViewKey,
|
||||
controller: _pageController,
|
||||
itemBuilder: _buildItems,
|
||||
itemCount: DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1,
|
||||
itemCount:
|
||||
widget.calendarDelegate.monthDelta(widget.firstDate, widget.lastDate) + 1,
|
||||
onPageChanged: _handleMonthPageChanged,
|
||||
),
|
||||
),
|
||||
@@ -843,13 +887,14 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||
/// 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});
|
||||
const _FocusedDate({required super.child, required this.calendarDelegate, this.date});
|
||||
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
final DateTime? date;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_FocusedDate oldWidget) {
|
||||
return !DateUtils.isSameDay(date, oldWidget.date);
|
||||
return !calendarDelegate.isSameDay(date, oldWidget.date);
|
||||
}
|
||||
|
||||
static DateTime? maybeOf(BuildContext context) {
|
||||
@@ -872,6 +917,7 @@ class _DayPicker extends StatefulWidget {
|
||||
required this.lastDate,
|
||||
required this.selectedDate,
|
||||
required this.onChanged,
|
||||
required this.calendarDelegate,
|
||||
this.selectableDayPredicate,
|
||||
}) : assert(!firstDate.isAfter(lastDate)),
|
||||
assert(selectedDate == null || !selectedDate.isBefore(firstDate)),
|
||||
@@ -904,6 +950,9 @@ class _DayPicker extends StatefulWidget {
|
||||
/// Optional user supplied predicate function to customize selectable days.
|
||||
final SelectableDayPredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
_DayPickerState createState() => _DayPickerState();
|
||||
}
|
||||
@@ -915,7 +964,7 @@ class _DayPickerState extends State<_DayPicker> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final int daysInMonth = DateUtils.getDaysInMonth(
|
||||
final int daysInMonth = widget.calendarDelegate.getDaysInMonth(
|
||||
widget.displayedMonth.year,
|
||||
widget.displayedMonth.month,
|
||||
);
|
||||
@@ -930,7 +979,8 @@ class _DayPickerState extends State<_DayPicker> {
|
||||
super.didChangeDependencies();
|
||||
// Check to see if the focused date is in this month, if so focus it.
|
||||
final DateTime? focusedDate = _FocusedDate.maybeOf(context);
|
||||
if (focusedDate != null && DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
|
||||
if (focusedDate != null &&
|
||||
widget.calendarDelegate.isSameMonth(widget.displayedMonth, focusedDate)) {
|
||||
_dayFocusNodes[focusedDate.day - 1].requestFocus();
|
||||
}
|
||||
}
|
||||
@@ -986,8 +1036,8 @@ class _DayPickerState extends State<_DayPicker> {
|
||||
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 int daysInMonth = widget.calendarDelegate.getDaysInMonth(year, month);
|
||||
final int dayOffset = widget.calendarDelegate.firstDayOffset(year, month, localizations);
|
||||
|
||||
final List<Widget> dayItems = _dayHeaders(weekdayStyle, localizations);
|
||||
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
|
||||
@@ -998,13 +1048,16 @@ class _DayPickerState extends State<_DayPicker> {
|
||||
if (day < 1) {
|
||||
dayItems.add(const SizedBox.shrink());
|
||||
} else {
|
||||
final DateTime dayToBuild = DateTime(year, month, day);
|
||||
final DateTime dayToBuild = widget.calendarDelegate.getDay(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);
|
||||
final bool isSelectedDay = widget.calendarDelegate.isSameDay(
|
||||
widget.selectedDate,
|
||||
dayToBuild,
|
||||
);
|
||||
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, dayToBuild);
|
||||
|
||||
dayItems.add(
|
||||
_Day(
|
||||
@@ -1015,6 +1068,7 @@ class _DayPickerState extends State<_DayPicker> {
|
||||
isToday: isToday,
|
||||
onChanged: widget.onChanged,
|
||||
focusNode: _dayFocusNodes[day - 1],
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1046,6 +1100,7 @@ class _Day extends StatefulWidget {
|
||||
required this.isToday,
|
||||
required this.onChanged,
|
||||
required this.focusNode,
|
||||
required this.calendarDelegate,
|
||||
});
|
||||
|
||||
final DateTime day;
|
||||
@@ -1054,6 +1109,7 @@ class _Day extends StatefulWidget {
|
||||
final bool isToday;
|
||||
final ValueChanged<DateTime> onChanged;
|
||||
final FocusNode focusNode;
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<_Day> createState() => _DayState();
|
||||
@@ -1146,7 +1202,7 @@ class _DayState extends State<_Day> {
|
||||
// for the day of month. To do that we prepend day of month to the
|
||||
// formatted full date.
|
||||
label:
|
||||
'${localizations.formatDecimal(widget.day.day)}, ${localizations.formatFullDate(widget.day)}$semanticLabelSuffix',
|
||||
'${localizations.formatDecimal(widget.day.day)}, ${widget.calendarDelegate.formatFullDate(widget.day, localizations)}$semanticLabelSuffix',
|
||||
// Set button to true to make the date selectable.
|
||||
button: true,
|
||||
selected: widget.isSelectedDay,
|
||||
@@ -1232,8 +1288,9 @@ class YearPicker extends StatefulWidget {
|
||||
required this.selectedDate,
|
||||
required this.onChanged,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) : assert(!firstDate.isAfter(lastDate)),
|
||||
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
|
||||
currentDate = calendarDelegate.dateOnly(currentDate ?? DateTime.now());
|
||||
|
||||
/// The current date.
|
||||
///
|
||||
@@ -1257,6 +1314,9 @@ class YearPicker extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<YearPicker> createState() => _YearPickerState();
|
||||
}
|
||||
@@ -1365,6 +1425,7 @@ class _YearPickerState extends State<YearPicker> {
|
||||
final TextStyle? itemStyle = (datePickerTheme.yearStyle ?? defaults.yearStyle)?.apply(
|
||||
color: textColor,
|
||||
);
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
Widget yearItem = Center(
|
||||
child: Container(
|
||||
decoration: decoration,
|
||||
@@ -1374,7 +1435,7 @@ class _YearPickerState extends State<YearPicker> {
|
||||
child: Semantics(
|
||||
selected: isSelected,
|
||||
button: true,
|
||||
child: Text(year.toString(), style: itemStyle),
|
||||
child: Text(widget.calendarDelegate.formatYear(year, localizations), style: itemStyle),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1382,15 +1443,20 @@ class _YearPickerState extends State<YearPicker> {
|
||||
if (isDisabled) {
|
||||
yearItem = ExcludeSemantics(child: yearItem);
|
||||
} else {
|
||||
DateTime date = DateTime(year, widget.selectedDate?.month ?? DateTime.january);
|
||||
if (date.isBefore(DateTime(widget.firstDate.year, widget.firstDate.month))) {
|
||||
DateTime date = widget.calendarDelegate.getMonth(
|
||||
year,
|
||||
widget.selectedDate?.month ?? DateTime.january,
|
||||
);
|
||||
if (date.isBefore(
|
||||
widget.calendarDelegate.getMonth(widget.firstDate.year, widget.firstDate.month),
|
||||
)) {
|
||||
// Ignore firstDate.day because we're just working in years and months here.
|
||||
assert(date.year == widget.firstDate.year);
|
||||
date = DateTime(year, widget.firstDate.month);
|
||||
date = widget.calendarDelegate.getMonth(year, widget.firstDate.month);
|
||||
} else if (date.isAfter(widget.lastDate)) {
|
||||
// No need to ignore the day here because it can only be bigger than what we care about.
|
||||
assert(date.year == widget.lastDate.year);
|
||||
date = DateTime(year, widget.lastDate.month);
|
||||
date = widget.calendarDelegate.getMonth(year, widget.lastDate.month);
|
||||
}
|
||||
_statesController.value = states;
|
||||
yearItem = InkWell(
|
||||
|
||||
@@ -8,38 +8,279 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'material_localizations.dart';
|
||||
|
||||
/// Controls the calendar system used in the date picker.
|
||||
///
|
||||
/// A [CalendarDelegate] defines how dates are interpreted, formatted, and
|
||||
/// navigated within the picker. Different calendar systems (e.g., Gregorian,
|
||||
/// Nepali, Hijri, Buddhist) can be supported by providing custom implementations.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example demonstrates how a [CalendarDelegate] is used to implement a
|
||||
/// custom calendar system in the date picker.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/date_picker/custom_calendar_date_picker.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GregorianCalendarDelegate], the default implementation for the Gregorian calendar.
|
||||
/// * [CalendarDatePicker], which uses this delegate to manage calendar-specific behavior.
|
||||
abstract class CalendarDelegate<T extends DateTime> {
|
||||
/// Creates a calendar delegate.
|
||||
const CalendarDelegate();
|
||||
|
||||
/// Returns a [DateTime] representing the current date and time.
|
||||
T now();
|
||||
|
||||
/// {@macro flutter.material.date.dateOnly}
|
||||
T dateOnly(T date);
|
||||
|
||||
/// {@macro flutter.material.date.datesOnly}
|
||||
DateTimeRange<T> datesOnly(DateTimeRange<T> range) {
|
||||
return DateTimeRange<T>(start: dateOnly(range.start), end: dateOnly(range.end));
|
||||
}
|
||||
|
||||
/// {@macro flutter.material.date.isSameDay}
|
||||
bool isSameDay(T? dateA, T? dateB) {
|
||||
return dateA?.year == dateB?.year && dateA?.month == dateB?.month && dateA?.day == dateB?.day;
|
||||
}
|
||||
|
||||
/// {@macro flutter.material.date.isSameMonth}
|
||||
bool isSameMonth(T? dateA, T? dateB) {
|
||||
return dateA?.year == dateB?.year && dateA?.month == dateB?.month;
|
||||
}
|
||||
|
||||
/// {@macro flutter.material.date.monthDelta}
|
||||
int monthDelta(T startDate, T endDate);
|
||||
|
||||
/// {@macro flutter.material.date.addMonthsToMonthDate}
|
||||
T addMonthsToMonthDate(T monthDate, int monthsToAdd);
|
||||
|
||||
/// {@macro flutter.material.date.addDaysToDate}
|
||||
T addDaysToDate(T date, int days);
|
||||
|
||||
/// {@macro flutter.material.date.firstDayOffset}
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations);
|
||||
|
||||
/// Returns the number of days in a month, according to the calendar system.
|
||||
int getDaysInMonth(int year, int month);
|
||||
|
||||
/// Returns a [DateTime] with the given [year] and [month].
|
||||
T getMonth(int year, int month);
|
||||
|
||||
/// Returns a [DateTime] with the given [year], [month], and [day].
|
||||
T getDay(int year, int month, int day);
|
||||
|
||||
/// Formats the month and the year of the given [date].
|
||||
///
|
||||
/// The returned string does not contain the day of the month. This appears
|
||||
/// in the date picker invoked using [showDatePicker].
|
||||
String formatMonthYear(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Full unabbreviated year format, e.g. 2017 rather than 17.
|
||||
String formatYear(int year, MaterialLocalizations localizations) {
|
||||
return localizations.formatYear(DateTime(year));
|
||||
}
|
||||
|
||||
/// Formats the date using a medium-width format.
|
||||
///
|
||||
/// Abbreviates month and days of week. This appears in the header of the date
|
||||
/// picker invoked using [showDatePicker].
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: Wed, Sep 27
|
||||
/// - Russian: ср, сент. 27
|
||||
String formatMediumDate(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Formats the month and day of the given [date].
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: Feb 21
|
||||
/// - Russian: 21 февр.
|
||||
String formatShortMonthDay(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Formats the date using a short-width format.
|
||||
///
|
||||
/// Includes the abbreviation of the month, the day and year.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: Feb 21, 2019
|
||||
/// - Russian: 21 февр. 2019 г.
|
||||
String formatShortDate(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Formats day of week, month, day of month and year in a long-width format.
|
||||
///
|
||||
/// Does not abbreviate names. Appears in spoken announcements of the date
|
||||
/// picker invoked using [showDatePicker], when accessibility mode is on.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: Wednesday, September 27, 2017
|
||||
/// - Russian: Среда, Сентябрь 27, 2017
|
||||
String formatFullDate(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Formats the date in a compact format.
|
||||
///
|
||||
/// Usually just the numeric values for the for day, month and year are used.
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// - US English: 02/21/2019
|
||||
/// - Russian: 21.02.2019
|
||||
///
|
||||
/// See also:
|
||||
/// * [parseCompactDate], which will convert a compact date string to a [DateTime].
|
||||
String formatCompactDate(T date, MaterialLocalizations localizations);
|
||||
|
||||
/// Converts the given compact date formatted string into a [DateTime].
|
||||
///
|
||||
/// The format of the string must be a valid compact date format for the
|
||||
/// given locale. If the text doesn't represent a valid date, `null` will be
|
||||
/// returned.
|
||||
///
|
||||
/// See also:
|
||||
/// * [formatCompactDate], which will convert a [DateTime] into a string in the compact format.
|
||||
T? parseCompactDate(String? inputString, MaterialLocalizations localizations);
|
||||
|
||||
/// The help text used on an empty [InputDatePickerFormField] to indicate
|
||||
/// to the user the date format being asked for.
|
||||
String dateHelpText(MaterialLocalizations localizations);
|
||||
}
|
||||
|
||||
/// A [CalendarDelegate] implementation for the Gregorian calendar system.
|
||||
///
|
||||
/// The Gregorian calendar is the most widely used civil calendar worldwide.
|
||||
/// This delegate provides standard date interpretation, formatting, and
|
||||
/// navigation based on the Gregorian system.
|
||||
///
|
||||
/// This delegate is the default calendar system for [CalendarDatePicker].
|
||||
///
|
||||
/// See also:
|
||||
/// * [CalendarDelegate], the base class for defining custom calendars.
|
||||
/// * [CalendarDatePicker], which uses this delegate for date selection.
|
||||
class GregorianCalendarDelegate extends CalendarDelegate<DateTime> {
|
||||
/// Creates a calendar delegate that uses the Gregorian calendar and the
|
||||
/// conventions of the current [MaterialLocalizations].
|
||||
const GregorianCalendarDelegate();
|
||||
|
||||
@override
|
||||
DateTime now() => DateTime.now();
|
||||
|
||||
@override
|
||||
DateTime dateOnly(DateTime date) => DateUtils.dateOnly(date);
|
||||
|
||||
@override
|
||||
int monthDelta(DateTime startDate, DateTime endDate) => DateUtils.monthDelta(startDate, endDate);
|
||||
|
||||
@override
|
||||
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
|
||||
return DateUtils.addMonthsToMonthDate(monthDate, monthsToAdd);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime addDaysToDate(DateTime date, int days) => DateUtils.addDaysToDate(date, days);
|
||||
|
||||
@override
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
return DateUtils.firstDayOffset(year, month, localizations);
|
||||
}
|
||||
|
||||
/// {@macro flutter.material.date.getDaysInMonth}
|
||||
@override
|
||||
int getDaysInMonth(int year, int month) => DateUtils.getDaysInMonth(year, month);
|
||||
|
||||
@override
|
||||
DateTime getMonth(int year, int month) => DateTime(year, month);
|
||||
|
||||
@override
|
||||
DateTime getDay(int year, int month, int day) => DateTime(year, month, day);
|
||||
|
||||
@override
|
||||
String formatMonthYear(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatMonthYear(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatMediumDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatMediumDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatShortMonthDay(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatShortMonthDay(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatShortDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatShortDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatFullDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatFullDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
String formatCompactDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return localizations.formatCompactDate(date);
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) {
|
||||
return localizations.parseCompactDate(inputString);
|
||||
}
|
||||
|
||||
@override
|
||||
String dateHelpText(MaterialLocalizations localizations) {
|
||||
return localizations.dateHelpText;
|
||||
}
|
||||
}
|
||||
|
||||
/// Utility functions for working with dates.
|
||||
abstract final class DateUtils {
|
||||
/// {@template flutter.material.date.dateOnly}
|
||||
/// Returns a [DateTime] with the date of the original, but time set to
|
||||
/// midnight.
|
||||
/// {@endtemplate}
|
||||
static DateTime dateOnly(DateTime date) {
|
||||
return DateTime(date.year, date.month, date.day);
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.datesOnly}
|
||||
/// Returns a [DateTimeRange] with the dates of the original, but with times
|
||||
/// set to midnight.
|
||||
///
|
||||
/// See also:
|
||||
/// * [dateOnly], which does the same thing for a single date.
|
||||
/// {@endtemplate}
|
||||
static DateTimeRange datesOnly(DateTimeRange range) {
|
||||
return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end));
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.isSameDay}
|
||||
/// Returns true if the two [DateTime] objects have the same day, month, and
|
||||
/// year, or are both null.
|
||||
/// {@endtemplate}
|
||||
static bool isSameDay(DateTime? dateA, DateTime? dateB) {
|
||||
return dateA?.year == dateB?.year && dateA?.month == dateB?.month && dateA?.day == dateB?.day;
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.isSameMonth}
|
||||
/// Returns true if the two [DateTime] objects have the same month and
|
||||
/// year, or are both null.
|
||||
/// {@endtemplate}
|
||||
static bool isSameMonth(DateTime? dateA, DateTime? dateB) {
|
||||
return dateA?.year == dateB?.year && dateA?.month == dateB?.month;
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.monthDelta}
|
||||
/// Determines the number of months between two [DateTime] objects.
|
||||
///
|
||||
/// For example:
|
||||
@@ -51,10 +292,12 @@ abstract final class DateUtils {
|
||||
/// ```
|
||||
///
|
||||
/// The value for `delta` would be `7`.
|
||||
/// {@endtemplate}
|
||||
static int monthDelta(DateTime startDate, DateTime endDate) {
|
||||
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.addMonthsToMonthDate}
|
||||
/// Returns a [DateTime] that is [monthDate] with the added number
|
||||
/// of months and the day set to 1 and time set to midnight.
|
||||
///
|
||||
@@ -67,16 +310,20 @@ abstract final class DateUtils {
|
||||
///
|
||||
/// `date` would be January 15, 2019.
|
||||
/// `futureDate` would be April 1, 2019 since it adds 3 months.
|
||||
/// {@endtemplate}
|
||||
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
|
||||
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.addDaysToDate}
|
||||
/// Returns a [DateTime] with the added number of days and time set to
|
||||
/// midnight.
|
||||
/// {@endtemplate}
|
||||
static DateTime addDaysToDate(DateTime date, int days) {
|
||||
return DateTime(date.year, date.month, date.day + days);
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.firstDayOffset}
|
||||
/// Computes the offset from the first day of the week that the first day of
|
||||
/// the [month] falls on.
|
||||
///
|
||||
@@ -105,6 +352,7 @@ abstract final class DateUtils {
|
||||
/// into the [MaterialLocalizations.narrowWeekdays] list.
|
||||
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
|
||||
/// days of week, always starting with Sunday and ending with Saturday.
|
||||
/// {@endtemplate}
|
||||
static int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
// 0-based day of week for the month and year, with 0 representing Monday.
|
||||
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
|
||||
@@ -121,11 +369,13 @@ abstract final class DateUtils {
|
||||
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
|
||||
}
|
||||
|
||||
/// {@template flutter.material.date.getDaysInMonth}
|
||||
/// Returns the number of days in a month, according to the proleptic
|
||||
/// Gregorian calendar.
|
||||
///
|
||||
/// This applies the leap year logic introduced by the Gregorian reforms of
|
||||
/// 1582. It will not give valid results for dates prior to that time.
|
||||
/// {@endtemplate}
|
||||
static int getDaysInMonth(int year, int month) {
|
||||
if (month == DateTime.february) {
|
||||
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
|
||||
@@ -203,15 +453,16 @@ typedef SelectableDayPredicate = bool Function(DateTime day);
|
||||
/// * [showDateRangePicker], which displays a dialog that allows the user to
|
||||
/// select a date range.
|
||||
@immutable
|
||||
class DateTimeRange {
|
||||
@optionalTypeArgs
|
||||
class DateTimeRange<T extends DateTime> {
|
||||
/// Creates a date range for the given start and end [DateTime].
|
||||
DateTimeRange({required this.start, required this.end}) : assert(!start.isAfter(end));
|
||||
|
||||
/// The start of the range of dates.
|
||||
final DateTime start;
|
||||
final T start;
|
||||
|
||||
/// The end of the range of dates.
|
||||
final DateTime end;
|
||||
final T end;
|
||||
|
||||
/// Returns a [Duration] of the time between [start] and [end].
|
||||
///
|
||||
|
||||
@@ -123,6 +123,8 @@ const double _fontSizeToScale = 14.0;
|
||||
/// this can be used to only allow weekdays for selection. If provided, it must
|
||||
/// return true for [initialDate].
|
||||
///
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
///
|
||||
/// The following optional string parameters allow you to override the default
|
||||
/// text used for various parts of the dialog:
|
||||
///
|
||||
@@ -221,10 +223,11 @@ Future<DateTime?> showDatePicker({
|
||||
final ValueChanged<DatePickerEntryMode>? onDatePickerModeChange,
|
||||
final Icon? switchToInputEntryModeIcon,
|
||||
final Icon? switchToCalendarEntryModeIcon,
|
||||
final CalendarDelegate<DateTime> calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) async {
|
||||
initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate);
|
||||
firstDate = DateUtils.dateOnly(firstDate);
|
||||
lastDate = DateUtils.dateOnly(lastDate);
|
||||
initialDate = initialDate == null ? null : calendarDelegate.dateOnly(initialDate);
|
||||
firstDate = calendarDelegate.dateOnly(firstDate);
|
||||
lastDate = calendarDelegate.dateOnly(lastDate);
|
||||
assert(
|
||||
!lastDate.isBefore(firstDate),
|
||||
'lastDate $lastDate must be on or after firstDate $firstDate.',
|
||||
@@ -262,6 +265,7 @@ Future<DateTime?> showDatePicker({
|
||||
onDatePickerModeChange: onDatePickerModeChange,
|
||||
switchToInputEntryModeIcon: switchToInputEntryModeIcon,
|
||||
switchToCalendarEntryModeIcon: switchToCalendarEntryModeIcon,
|
||||
calendarDelegate: calendarDelegate,
|
||||
);
|
||||
|
||||
if (textDirection != null) {
|
||||
@@ -328,10 +332,11 @@ class DatePickerDialog extends StatefulWidget {
|
||||
this.switchToInputEntryModeIcon,
|
||||
this.switchToCalendarEntryModeIcon,
|
||||
this.insetPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
|
||||
}) : initialDate = initialDate == null ? null : DateUtils.dateOnly(initialDate),
|
||||
firstDate = DateUtils.dateOnly(firstDate),
|
||||
lastDate = DateUtils.dateOnly(lastDate),
|
||||
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
|
||||
this.calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) : initialDate = initialDate == null ? null : calendarDelegate.dateOnly(initialDate),
|
||||
firstDate = calendarDelegate.dateOnly(firstDate),
|
||||
lastDate = calendarDelegate.dateOnly(lastDate),
|
||||
currentDate = calendarDelegate.dateOnly(currentDate ?? calendarDelegate.now()) {
|
||||
assert(
|
||||
!this.lastDate.isBefore(this.firstDate),
|
||||
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
|
||||
@@ -453,6 +458,9 @@ class DatePickerDialog extends StatefulWidget {
|
||||
/// Defaults to `EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0)`.
|
||||
final EdgeInsets insetPadding;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<DatePickerDialog> createState() => _DatePickerDialogState();
|
||||
}
|
||||
@@ -623,6 +631,7 @@ class _DatePickerDialogState extends State<DatePickerDialog> with RestorationMix
|
||||
|
||||
CalendarDatePicker calendarDatePicker() {
|
||||
return CalendarDatePicker(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
key: _calendarPickerKey,
|
||||
initialDate: _selectedDate.value,
|
||||
firstDate: widget.firstDate,
|
||||
@@ -654,6 +663,7 @@ class _DatePickerDialogState extends State<DatePickerDialog> with RestorationMix
|
||||
child: MediaQuery.withClampedTextScaling(
|
||||
maxScaleFactor: 2.0,
|
||||
child: InputDatePickerFormField(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
initialDate: _selectedDate.value,
|
||||
firstDate: widget.firstDate,
|
||||
lastDate: widget.lastDate,
|
||||
@@ -716,7 +726,9 @@ class _DatePickerDialogState extends State<DatePickerDialog> with RestorationMix
|
||||
? localizations.datePickerHelpText
|
||||
: localizations.datePickerHelpText.toUpperCase()),
|
||||
titleText:
|
||||
_selectedDate.value == null ? '' : localizations.formatMediumDate(_selectedDate.value!),
|
||||
_selectedDate.value == null
|
||||
? ''
|
||||
: widget.calendarDelegate.formatMediumDate(_selectedDate.value!, localizations),
|
||||
titleStyle: headlineStyle,
|
||||
orientation: orientation,
|
||||
isShort: orientation == Orientation.landscape,
|
||||
@@ -1076,6 +1088,8 @@ typedef SelectableDayForRangePredicate =
|
||||
///
|
||||
/// {@macro flutter.material.date_picker.switchToCalendarEntryModeIcon}
|
||||
///
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
///
|
||||
/// The following optional string parameters allow you to override the default
|
||||
/// text used for various parts of the dialog:
|
||||
///
|
||||
@@ -1173,10 +1187,11 @@ Future<DateTimeRange?> showDateRangePicker({
|
||||
final Icon? switchToInputEntryModeIcon,
|
||||
final Icon? switchToCalendarEntryModeIcon,
|
||||
SelectableDayForRangePredicate? selectableDayPredicate,
|
||||
CalendarDelegate<DateTime> calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) async {
|
||||
initialDateRange = initialDateRange == null ? null : DateUtils.datesOnly(initialDateRange);
|
||||
firstDate = DateUtils.dateOnly(firstDate);
|
||||
lastDate = DateUtils.dateOnly(lastDate);
|
||||
initialDateRange = initialDateRange == null ? null : calendarDelegate.datesOnly(initialDateRange);
|
||||
firstDate = calendarDelegate.dateOnly(firstDate);
|
||||
lastDate = calendarDelegate.dateOnly(lastDate);
|
||||
assert(
|
||||
!lastDate.isBefore(firstDate),
|
||||
'lastDate $lastDate must be on or after firstDate $firstDate.',
|
||||
@@ -1213,7 +1228,7 @@ Future<DateTimeRange?> showDateRangePicker({
|
||||
selectableDayPredicate(initialDateRange.end, initialDateRange.start, initialDateRange.end),
|
||||
"initialDateRange's end date must be selectable.",
|
||||
);
|
||||
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now());
|
||||
currentDate = calendarDelegate.dateOnly(currentDate ?? calendarDelegate.now());
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
|
||||
Widget dialog = DateRangePickerDialog(
|
||||
@@ -1270,14 +1285,15 @@ Future<DateTimeRange?> showDateRangePicker({
|
||||
/// (i.e. 'Jan 21, 2020').
|
||||
String _formatRangeStartDate(
|
||||
MaterialLocalizations localizations,
|
||||
CalendarDelegate<DateTime> calendarDelegate,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
) {
|
||||
return startDate == null
|
||||
? localizations.dateRangeStartLabel
|
||||
: (endDate == null || startDate.year == endDate.year)
|
||||
? localizations.formatShortMonthDay(startDate)
|
||||
: localizations.formatShortDate(startDate);
|
||||
? calendarDelegate.formatShortMonthDay(startDate, localizations)
|
||||
: calendarDelegate.formatShortDate(startDate, localizations);
|
||||
}
|
||||
|
||||
/// Returns an locale-appropriate string to describe the end of a date range.
|
||||
@@ -1288,6 +1304,7 @@ String _formatRangeStartDate(
|
||||
/// include the year (i.e. 'Jan 21, 2020').
|
||||
String _formatRangeEndDate(
|
||||
MaterialLocalizations localizations,
|
||||
CalendarDelegate<DateTime> calendarDelegate,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
DateTime currentDate,
|
||||
@@ -1295,8 +1312,8 @@ String _formatRangeEndDate(
|
||||
return endDate == null
|
||||
? localizations.dateRangeEndLabel
|
||||
: (startDate != null && startDate.year == endDate.year && startDate.year == currentDate.year)
|
||||
? localizations.formatShortMonthDay(endDate)
|
||||
: localizations.formatShortDate(endDate);
|
||||
? calendarDelegate.formatShortMonthDay(endDate, localizations)
|
||||
: calendarDelegate.formatShortDate(endDate, localizations);
|
||||
}
|
||||
|
||||
/// A Material-style date range picker dialog.
|
||||
@@ -1333,6 +1350,7 @@ class DateRangePickerDialog extends StatefulWidget {
|
||||
this.switchToInputEntryModeIcon,
|
||||
this.switchToCalendarEntryModeIcon,
|
||||
this.selectableDayPredicate,
|
||||
this.calendarDelegate = const GregorianCalendarDelegate(),
|
||||
});
|
||||
|
||||
/// The date range that the date range picker starts with when it opens.
|
||||
@@ -1464,6 +1482,9 @@ class DateRangePickerDialog extends StatefulWidget {
|
||||
/// Function to provide full control over which [DateTime] can be selected.
|
||||
final SelectableDayForRangePredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<DateRangePickerDialog> createState() => _DateRangePickerDialogState();
|
||||
}
|
||||
@@ -1598,6 +1619,7 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
|
||||
case DatePickerEntryMode.calendarOnly:
|
||||
contents = _CalendarRangePickerDialog(
|
||||
key: _calendarPickerKey,
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
selectedStartDate: _selectedStart.value,
|
||||
selectedEndDate: _selectedEnd.value,
|
||||
firstDate: widget.firstDate,
|
||||
@@ -1641,6 +1663,7 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
|
||||
case DatePickerEntryMode.input:
|
||||
case DatePickerEntryMode.inputOnly:
|
||||
contents = _InputDateRangePickerDialog(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
selectedStartDate: _selectedStart.value,
|
||||
selectedEndDate: _selectedEnd.value,
|
||||
currentDate: widget.currentDate,
|
||||
@@ -1656,6 +1679,7 @@ class _DateRangePickerDialogState extends State<DateRangePickerDialog> with Rest
|
||||
const Spacer(),
|
||||
_InputDateRangePicker(
|
||||
key: _inputPickerKey,
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
initialStartDate: _selectedStart.value,
|
||||
initialEndDate: _selectedEnd.value,
|
||||
firstDate: widget.firstDate,
|
||||
@@ -1763,6 +1787,7 @@ class _CalendarRangePickerDialog extends StatelessWidget {
|
||||
required this.confirmText,
|
||||
required this.helpText,
|
||||
required this.selectableDayPredicate,
|
||||
required this.calendarDelegate,
|
||||
this.entryModeButton,
|
||||
});
|
||||
|
||||
@@ -1778,6 +1803,7 @@ class _CalendarRangePickerDialog extends StatelessWidget {
|
||||
final VoidCallback? onCancel;
|
||||
final String confirmText;
|
||||
final String helpText;
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
final Widget? entryModeButton;
|
||||
|
||||
@override
|
||||
@@ -1802,14 +1828,16 @@ class _CalendarRangePickerDialog extends StatelessWidget {
|
||||
?.apply(color: headerForeground);
|
||||
final String startDateText = _formatRangeStartDate(
|
||||
localizations,
|
||||
calendarDelegate,
|
||||
selectedStartDate,
|
||||
selectedEndDate,
|
||||
);
|
||||
final String endDateText = _formatRangeEndDate(
|
||||
localizations,
|
||||
calendarDelegate,
|
||||
selectedStartDate,
|
||||
selectedEndDate,
|
||||
DateTime.now(),
|
||||
calendarDelegate.now(),
|
||||
);
|
||||
final TextStyle? startDateStyle = headlineStyle?.apply(
|
||||
color: selectedStartDate != null ? headerForeground : headerDisabledForeground,
|
||||
@@ -1902,6 +1930,7 @@ class _CalendarRangePickerDialog extends StatelessWidget {
|
||||
onStartDateChanged: onStartDateChanged,
|
||||
onEndDateChanged: onEndDateChanged,
|
||||
selectableDayPredicate: selectableDayPredicate,
|
||||
calendarDelegate: calendarDelegate,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1931,11 +1960,13 @@ class _CalendarDateRangePicker extends StatefulWidget {
|
||||
DateTime? currentDate,
|
||||
required this.onStartDateChanged,
|
||||
required this.onEndDateChanged,
|
||||
}) : initialStartDate = initialStartDate != null ? DateUtils.dateOnly(initialStartDate) : null,
|
||||
initialEndDate = initialEndDate != null ? DateUtils.dateOnly(initialEndDate) : null,
|
||||
firstDate = DateUtils.dateOnly(firstDate),
|
||||
lastDate = DateUtils.dateOnly(lastDate),
|
||||
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()) {
|
||||
required this.calendarDelegate,
|
||||
}) : initialStartDate =
|
||||
initialStartDate != null ? calendarDelegate.dateOnly(initialStartDate) : null,
|
||||
initialEndDate = initialEndDate != null ? calendarDelegate.dateOnly(initialEndDate) : null,
|
||||
firstDate = calendarDelegate.dateOnly(firstDate),
|
||||
lastDate = calendarDelegate.dateOnly(lastDate),
|
||||
currentDate = calendarDelegate.dateOnly(currentDate ?? calendarDelegate.now()) {
|
||||
assert(
|
||||
this.initialStartDate == null ||
|
||||
this.initialEndDate == null ||
|
||||
@@ -1969,6 +2000,9 @@ class _CalendarDateRangePicker extends StatefulWidget {
|
||||
/// Called when the user changes the end date of the selected range.
|
||||
final ValueChanged<DateTime?>? onEndDateChanged;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<_CalendarDateRangePicker> createState() => _CalendarDateRangePickerState();
|
||||
}
|
||||
@@ -1994,7 +2028,7 @@ class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
|
||||
// divide the list of months into two `SliverList`s.
|
||||
final DateTime initialDate = widget.initialStartDate ?? widget.currentDate;
|
||||
if (!initialDate.isBefore(widget.firstDate) && !initialDate.isAfter(widget.lastDate)) {
|
||||
_initialMonthIndex = DateUtils.monthDelta(widget.firstDate, initialDate);
|
||||
_initialMonthIndex = widget.calendarDelegate.monthDelta(widget.firstDate, initialDate);
|
||||
}
|
||||
|
||||
_showWeekBottomDivider = _initialMonthIndex != 0;
|
||||
@@ -2018,7 +2052,8 @@ class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
|
||||
}
|
||||
}
|
||||
|
||||
int get _numberOfMonths => DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1;
|
||||
int get _numberOfMonths =>
|
||||
widget.calendarDelegate.monthDelta(widget.firstDate, widget.lastDate) + 1;
|
||||
|
||||
void _vibrate() {
|
||||
switch (Theme.of(context).platform) {
|
||||
@@ -2062,8 +2097,12 @@ class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
|
||||
Widget _buildMonthItem(BuildContext context, int index, bool beforeInitialMonth) {
|
||||
final int monthIndex =
|
||||
beforeInitialMonth ? _initialMonthIndex - index - 1 : _initialMonthIndex + index;
|
||||
final DateTime month = DateUtils.addMonthsToMonthDate(widget.firstDate, monthIndex);
|
||||
final DateTime month = widget.calendarDelegate.addMonthsToMonthDate(
|
||||
widget.firstDate,
|
||||
monthIndex,
|
||||
);
|
||||
return _MonthItem(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
selectedDateStart: _startDate,
|
||||
selectedDateEnd: _endDate,
|
||||
currentDate: widget.currentDate,
|
||||
@@ -2085,6 +2124,7 @@ class _CalendarDateRangePickerState extends State<_CalendarDateRangePicker> {
|
||||
if (_showWeekBottomDivider) const Divider(height: 0),
|
||||
Expanded(
|
||||
child: _CalendarKeyboardNavigator(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
firstDate: widget.firstDate,
|
||||
lastDate: widget.lastDate,
|
||||
initialFocusedDay: _startDate ?? widget.initialStartDate ?? widget.currentDate,
|
||||
@@ -2125,12 +2165,14 @@ class _CalendarKeyboardNavigator extends StatefulWidget {
|
||||
required this.firstDate,
|
||||
required this.lastDate,
|
||||
required this.initialFocusedDay,
|
||||
required this.calendarDelegate,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final DateTime firstDate;
|
||||
final DateTime lastDate;
|
||||
final DateTime initialFocusedDay;
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
_CalendarKeyboardNavigatorState createState() => _CalendarKeyboardNavigatorState();
|
||||
@@ -2231,7 +2273,7 @@ class _CalendarKeyboardNavigatorState extends State<_CalendarKeyboardNavigator>
|
||||
|
||||
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final DateTime nextDate = DateUtils.addDaysToDate(
|
||||
final DateTime nextDate = widget.calendarDelegate.addDaysToDate(
|
||||
date,
|
||||
_dayDirectionOffset(direction, textDirection),
|
||||
);
|
||||
@@ -2249,6 +2291,7 @@ class _CalendarKeyboardNavigatorState extends State<_CalendarKeyboardNavigator>
|
||||
focusNode: _dayGridFocus,
|
||||
onFocusChange: _handleGridFocusChange,
|
||||
child: _FocusedDate(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
date: _dayGridFocus.hasFocus ? _focusedDay : null,
|
||||
scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null,
|
||||
child: widget.child,
|
||||
@@ -2260,14 +2303,20 @@ class _CalendarKeyboardNavigatorState extends State<_CalendarKeyboardNavigator>
|
||||
/// InheritedWidget indicating what the current focused date is for its children.
|
||||
// See also: _FocusedDate in calendar_date_picker.dart
|
||||
class _FocusedDate extends InheritedWidget {
|
||||
const _FocusedDate({required super.child, this.date, this.scrollDirection});
|
||||
const _FocusedDate({
|
||||
required super.child,
|
||||
required this.calendarDelegate,
|
||||
this.date,
|
||||
this.scrollDirection,
|
||||
});
|
||||
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
final DateTime? date;
|
||||
final TraversalDirection? scrollDirection;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_FocusedDate oldWidget) {
|
||||
return !DateUtils.isSameDay(date, oldWidget.date) ||
|
||||
return !calendarDelegate.isSameDay(date, oldWidget.date) ||
|
||||
scrollDirection != oldWidget.scrollDirection;
|
||||
}
|
||||
|
||||
@@ -2464,6 +2513,7 @@ class _MonthItem extends StatefulWidget {
|
||||
required this.lastDate,
|
||||
required this.displayedMonth,
|
||||
required this.selectableDayPredicate,
|
||||
required this.calendarDelegate,
|
||||
}) : assert(!firstDate.isAfter(lastDate)),
|
||||
assert(selectedDateStart == null || !selectedDateStart.isBefore(firstDate)),
|
||||
assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
|
||||
@@ -2502,6 +2552,9 @@ class _MonthItem extends StatefulWidget {
|
||||
|
||||
final SelectableDayForRangePredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
_MonthItemState createState() => _MonthItemState();
|
||||
}
|
||||
@@ -2513,7 +2566,7 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final int daysInMonth = DateUtils.getDaysInMonth(
|
||||
final int daysInMonth = widget.calendarDelegate.getDaysInMonth(
|
||||
widget.displayedMonth.year,
|
||||
widget.displayedMonth.month,
|
||||
);
|
||||
@@ -2528,7 +2581,8 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
super.didChangeDependencies();
|
||||
// Check to see if the focused date is in this month, if so focus it.
|
||||
final DateTime? focusedDate = _FocusedDate.maybeOf(context)?.date;
|
||||
if (focusedDate != null && DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
|
||||
if (focusedDate != null &&
|
||||
widget.calendarDelegate.isSameMonth(widget.displayedMonth, focusedDate)) {
|
||||
_dayFocusNodes[focusedDate.day - 1].requestFocus();
|
||||
}
|
||||
}
|
||||
@@ -2596,9 +2650,10 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
dayToBuild.isBefore(widget.selectedDateEnd!);
|
||||
final bool isOneDayRange =
|
||||
isRangeSelected && widget.selectedDateStart == widget.selectedDateEnd;
|
||||
final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild);
|
||||
final bool isToday = widget.calendarDelegate.isSameDay(widget.currentDate, dayToBuild);
|
||||
|
||||
return _DayItem(
|
||||
calendarDelegate: widget.calendarDelegate,
|
||||
day: dayToBuild,
|
||||
focusNode: _dayFocusNodes[day - 1],
|
||||
onChanged: widget.onChanged,
|
||||
@@ -2626,8 +2681,8 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
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 int daysInMonth = widget.calendarDelegate.getDaysInMonth(year, month);
|
||||
final int dayOffset = widget.calendarDelegate.firstDayOffset(year, month, localizations);
|
||||
final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
|
||||
final double gridHeight =
|
||||
weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
|
||||
@@ -2639,7 +2694,7 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
if (day < 1) {
|
||||
dayItems.add(const LimitedBox(maxWidth: 0.0, maxHeight: 0.0, child: SizedBox.expand()));
|
||||
} else {
|
||||
final DateTime dayToBuild = DateTime(year, month, day);
|
||||
final DateTime dayToBuild = widget.calendarDelegate.getDay(year, month, day);
|
||||
final Widget dayItem = _buildDayItem(context, dayToBuild, dayOffset, daysInMonth);
|
||||
dayItems.add(dayItem);
|
||||
}
|
||||
@@ -2653,7 +2708,11 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
final int end = math.min(start + DateTime.daysPerWeek, dayItems.length);
|
||||
final List<Widget> weekList = dayItems.sublist(start, end);
|
||||
|
||||
final DateTime dateAfterLeadingPadding = DateTime(year, month, start - dayOffset + 1);
|
||||
final DateTime dateAfterLeadingPadding = widget.calendarDelegate.getDay(
|
||||
year,
|
||||
month,
|
||||
start - dayOffset + 1,
|
||||
);
|
||||
// Only color the edge container if it is after the start date and
|
||||
// on/before the end date.
|
||||
final bool isLeadingInRange =
|
||||
@@ -2668,7 +2727,11 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
// partial week.
|
||||
if (end < dayItems.length ||
|
||||
(end == dayItems.length && dayItems.length % DateTime.daysPerWeek == 0)) {
|
||||
final DateTime dateBeforeTrailingPadding = DateTime(year, month, end - dayOffset);
|
||||
final DateTime dateBeforeTrailingPadding = widget.calendarDelegate.getDay(
|
||||
year,
|
||||
month,
|
||||
end - dayOffset,
|
||||
);
|
||||
// Only color the edge container if it is on/after the start date and
|
||||
// before the end date.
|
||||
final bool isTrailingInRange =
|
||||
@@ -2696,7 +2759,7 @@ class _MonthItemState extends State<_MonthItem> {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
localizations.formatMonthYear(widget.displayedMonth),
|
||||
widget.calendarDelegate.formatMonthYear(widget.displayedMonth, localizations),
|
||||
style: textTheme.bodyMedium!.apply(color: themeData.colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
@@ -2731,6 +2794,7 @@ class _DayItem extends StatefulWidget {
|
||||
required this.isInRange,
|
||||
required this.isOneDayRange,
|
||||
required this.isToday,
|
||||
required this.calendarDelegate,
|
||||
});
|
||||
|
||||
final DateTime day;
|
||||
@@ -2757,6 +2821,8 @@ class _DayItem extends StatefulWidget {
|
||||
|
||||
final bool isToday;
|
||||
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<_DayItem> createState() => _DayItemState();
|
||||
}
|
||||
@@ -2872,7 +2938,7 @@ class _DayItemState extends State<_DayItem> {
|
||||
// formatted full date.
|
||||
final String semanticLabelSuffix = widget.isToday ? ', ${localizations.currentDateLabel}' : '';
|
||||
String semanticLabel =
|
||||
'$dayText, ${localizations.formatFullDate(widget.day)}$semanticLabelSuffix';
|
||||
'$dayText, ${widget.calendarDelegate.formatFullDate(widget.day, localizations)}$semanticLabelSuffix';
|
||||
if (widget.isSelectedDayStart) {
|
||||
semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel);
|
||||
} else if (widget.isSelectedDayEnd) {
|
||||
@@ -2987,6 +3053,7 @@ class _InputDateRangePickerDialog extends StatelessWidget {
|
||||
required this.cancelText,
|
||||
required this.helpText,
|
||||
required this.entryModeButton,
|
||||
required this.calendarDelegate,
|
||||
});
|
||||
|
||||
final DateTime? selectedStartDate;
|
||||
@@ -2999,11 +3066,12 @@ class _InputDateRangePickerDialog extends StatelessWidget {
|
||||
final String? cancelText;
|
||||
final String? helpText;
|
||||
final Widget? entryModeButton;
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
String _formatDateRange(BuildContext context, DateTime? start, DateTime? end, DateTime now) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final String startText = _formatRangeStartDate(localizations, start, end);
|
||||
final String endText = _formatRangeEndDate(localizations, start, end, now);
|
||||
final String startText = _formatRangeStartDate(localizations, calendarDelegate, start, end);
|
||||
final String endText = _formatRangeEndDate(localizations, calendarDelegate, start, end, now);
|
||||
if (start == null || end == null) {
|
||||
return localizations.unspecifiedDateRange;
|
||||
}
|
||||
@@ -3042,7 +3110,7 @@ class _InputDateRangePickerDialog extends StatelessWidget {
|
||||
);
|
||||
final String semanticDateText =
|
||||
selectedStartDate != null && selectedEndDate != null
|
||||
? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}'
|
||||
? '${calendarDelegate.formatMediumDate(selectedStartDate!, localizations)} – ${calendarDelegate.formatMediumDate(selectedEndDate!, localizations)}'
|
||||
: '';
|
||||
|
||||
final Widget header = _DatePickerHeader(
|
||||
@@ -3149,6 +3217,7 @@ class _InputDateRangePicker extends StatefulWidget {
|
||||
required this.onStartDateChanged,
|
||||
required this.onEndDateChanged,
|
||||
required this.selectableDayPredicate,
|
||||
required this.calendarDelegate,
|
||||
this.helpText,
|
||||
this.errorFormatText,
|
||||
this.errorInvalidText,
|
||||
@@ -3160,10 +3229,11 @@ class _InputDateRangePicker extends StatefulWidget {
|
||||
this.autofocus = false,
|
||||
this.autovalidate = false,
|
||||
this.keyboardType = TextInputType.datetime,
|
||||
}) : initialStartDate = initialStartDate == null ? null : DateUtils.dateOnly(initialStartDate),
|
||||
initialEndDate = initialEndDate == null ? null : DateUtils.dateOnly(initialEndDate),
|
||||
firstDate = DateUtils.dateOnly(firstDate),
|
||||
lastDate = DateUtils.dateOnly(lastDate);
|
||||
}) : initialStartDate =
|
||||
initialStartDate == null ? null : calendarDelegate.dateOnly(initialStartDate),
|
||||
initialEndDate = initialEndDate == null ? null : calendarDelegate.dateOnly(initialEndDate),
|
||||
firstDate = calendarDelegate.dateOnly(firstDate),
|
||||
lastDate = calendarDelegate.dateOnly(lastDate);
|
||||
|
||||
/// The [DateTime] that represents the start of the initial date range selection.
|
||||
final DateTime? initialStartDate;
|
||||
@@ -3224,6 +3294,9 @@ class _InputDateRangePicker extends StatefulWidget {
|
||||
|
||||
final SelectableDayForRangePredicate? selectableDayPredicate;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
_InputDateRangePickerState createState() => _InputDateRangePickerState();
|
||||
}
|
||||
@@ -3262,14 +3335,14 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
|
||||
super.didChangeDependencies();
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
if (_startDate != null) {
|
||||
_startInputText = localizations.formatCompactDate(_startDate!);
|
||||
_startInputText = widget.calendarDelegate.formatCompactDate(_startDate!, localizations);
|
||||
final bool selectText = widget.autofocus && !_autoSelected;
|
||||
_updateController(_startController, _startInputText, selectText);
|
||||
_autoSelected = selectText;
|
||||
}
|
||||
|
||||
if (_endDate != null) {
|
||||
_endInputText = localizations.formatCompactDate(_endDate!);
|
||||
_endInputText = widget.calendarDelegate.formatCompactDate(_endDate!, localizations);
|
||||
_updateController(_endController, _endInputText, false);
|
||||
}
|
||||
}
|
||||
@@ -3298,7 +3371,7 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
|
||||
|
||||
DateTime? _parseDate(String? text) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
return localizations.parseCompactDate(text);
|
||||
return widget.calendarDelegate.parseCompactDate(text, localizations);
|
||||
}
|
||||
|
||||
String? _validateDate(DateTime? date) {
|
||||
@@ -3371,7 +3444,8 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
|
||||
decoration: InputDecoration(
|
||||
border: inputBorder,
|
||||
filled: inputTheme.filled,
|
||||
hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
|
||||
hintText:
|
||||
widget.fieldStartHintText ?? widget.calendarDelegate.dateHelpText(localizations),
|
||||
labelText: widget.fieldStartLabelText ?? localizations.dateRangeStartLabel,
|
||||
errorText: _startErrorText,
|
||||
),
|
||||
@@ -3387,7 +3461,8 @@ class _InputDateRangePickerState extends State<_InputDateRangePicker> {
|
||||
decoration: InputDecoration(
|
||||
border: inputBorder,
|
||||
filled: inputTheme.filled,
|
||||
hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
|
||||
hintText:
|
||||
widget.fieldEndHintText ?? widget.calendarDelegate.dateHelpText(localizations),
|
||||
labelText: widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
|
||||
errorText: _endErrorText,
|
||||
),
|
||||
|
||||
@@ -62,9 +62,10 @@ class InputDatePickerFormField extends StatefulWidget {
|
||||
this.autofocus = false,
|
||||
this.acceptEmptyDate = false,
|
||||
this.focusNode,
|
||||
}) : initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null,
|
||||
firstDate = DateUtils.dateOnly(firstDate),
|
||||
lastDate = DateUtils.dateOnly(lastDate) {
|
||||
this.calendarDelegate = const GregorianCalendarDelegate(),
|
||||
}) : initialDate = initialDate != null ? calendarDelegate.dateOnly(initialDate) : null,
|
||||
firstDate = calendarDelegate.dateOnly(firstDate),
|
||||
lastDate = calendarDelegate.dateOnly(lastDate) {
|
||||
assert(
|
||||
!this.lastDate.isBefore(this.firstDate),
|
||||
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.',
|
||||
@@ -146,6 +147,9 @@ class InputDatePickerFormField extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.material.calendar_date_picker.calendarDelegate}
|
||||
final CalendarDelegate<DateTime> calendarDelegate;
|
||||
|
||||
@override
|
||||
State<InputDatePickerFormField> createState() => _InputDatePickerFormFieldState();
|
||||
}
|
||||
@@ -191,7 +195,7 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||
void _updateValueForSelectedDate() {
|
||||
if (_selectedDate != null) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
_inputText = localizations.formatCompactDate(_selectedDate!);
|
||||
_inputText = widget.calendarDelegate.formatCompactDate(_selectedDate!, localizations);
|
||||
TextEditingValue textEditingValue = TextEditingValue(text: _inputText!);
|
||||
// Select the new text if we are auto focused and haven't selected the text before.
|
||||
if (widget.autofocus && !_autoSelected) {
|
||||
@@ -209,7 +213,7 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||
|
||||
DateTime? _parseDate(String? text) {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
return localizations.parseCompactDate(text);
|
||||
return widget.calendarDelegate.parseCompactDate(text, localizations);
|
||||
}
|
||||
|
||||
bool _isValidAcceptableDate(DateTime? date) {
|
||||
@@ -265,7 +269,7 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||
container: true,
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.fieldHintText ?? localizations.dateHelpText,
|
||||
hintText: widget.fieldHintText ?? widget.calendarDelegate.dateHelpText(localizations),
|
||||
labelText: widget.fieldLabelText ?? localizations.dateInputLabel,
|
||||
).applyDefaults(
|
||||
inputTheme
|
||||
|
||||
@@ -1493,4 +1493,79 @@ void main() {
|
||||
expect(selectedYear, equals(DateTime(2018, DateTime.june)));
|
||||
});
|
||||
});
|
||||
|
||||
group('Calendar Delegate', () {
|
||||
testWidgets('Defaults to Gregorian calendar system', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: CalendarDatePicker(
|
||||
initialDate: DateTime(2025, DateTime.february, 26),
|
||||
firstDate: DateTime(2025, DateTime.february),
|
||||
lastDate: DateTime(2025, DateTime.may),
|
||||
onDateChanged: (DateTime value) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final CalendarDatePicker calendarPicker = tester.widget(find.byType(CalendarDatePicker));
|
||||
expect(calendarPicker.calendarDelegate, isA<GregorianCalendarDelegate>());
|
||||
|
||||
final Finder datePickerModeToggleButton = find.descendant(
|
||||
of: find.byType(InkWell),
|
||||
matching: find.text('February 2025'),
|
||||
);
|
||||
await tester.tap(datePickerModeToggleButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final YearPicker yearPicker = tester.widget(find.byType(YearPicker));
|
||||
expect(yearPicker.calendarDelegate, isA<GregorianCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Using custom calendar delegate implementation', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: CalendarDatePicker(
|
||||
initialDate: DateTime(2025, DateTime.february, 26),
|
||||
firstDate: DateTime(2025, DateTime.february),
|
||||
lastDate: DateTime(2025, DateTime.may),
|
||||
onDateChanged: (DateTime value) {},
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final CalendarDatePicker calendarPicker = tester.widget(find.byType(CalendarDatePicker));
|
||||
expect(calendarPicker.calendarDelegate, isA<TestCalendarDelegate>());
|
||||
|
||||
final Finder datePickerModeToggleButton = find.descendant(
|
||||
of: find.byType(InkWell),
|
||||
matching: find.text('February 2025'),
|
||||
);
|
||||
await tester.tap(datePickerModeToggleButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final YearPicker yearPicker = tester.widget(find.byType(YearPicker));
|
||||
expect(yearPicker.calendarDelegate, isA<TestCalendarDelegate>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestCalendarDelegate extends GregorianCalendarDelegate {
|
||||
const TestCalendarDelegate();
|
||||
|
||||
@override
|
||||
int getDaysInMonth(int year, int month) {
|
||||
return month.isEven ? 21 : 28;
|
||||
}
|
||||
|
||||
@override
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2636,6 +2636,86 @@ void main() {
|
||||
await gesture.up();
|
||||
});
|
||||
});
|
||||
|
||||
group('Calendar Delegate', () {
|
||||
testWidgets('Defaults to Gregorian calendar system', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DatePickerDialog(
|
||||
initialDate: initialDate,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final DatePickerDialog dialog = tester.widget(find.byType(DatePickerDialog));
|
||||
expect(dialog.calendarDelegate, isA<GregorianCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Using custom calendar delegate implementation', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DatePickerDialog(
|
||||
initialDate: initialDate,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final DatePickerDialog dialog = tester.widget(find.byType(DatePickerDialog));
|
||||
expect(dialog.calendarDelegate, isA<TestCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Displays calendar based on the calendar delegate', (WidgetTester tester) async {
|
||||
Text getLastDayText() {
|
||||
final Finder dayFinder = find.descendant(of: find.byType(Ink), matching: find.byType(Text));
|
||||
return tester.widget(dayFinder.last);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DatePickerDialog(
|
||||
initialDate: initialDate,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder nextMonthButton = find.byIcon(Icons.chevron_right);
|
||||
|
||||
Text lastDayText = getLastDayText();
|
||||
expect(find.text('January 2016'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('28'));
|
||||
|
||||
await tester.tap(nextMonthButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
lastDayText = getLastDayText();
|
||||
expect(find.text('February 2016'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('21'));
|
||||
|
||||
await tester.tap(nextMonthButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
lastDayText = getLastDayText();
|
||||
expect(find.text('March 2016'), findsOneWidget);
|
||||
expect(lastDayText.data, equals('28'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _RestorableDatePickerDialogTestWidget extends StatefulWidget {
|
||||
@@ -2753,3 +2833,17 @@ class _DatePickerObserver extends NavigatorObserver {
|
||||
super.didPop(route, previousRoute);
|
||||
}
|
||||
}
|
||||
|
||||
class TestCalendarDelegate extends GregorianCalendarDelegate {
|
||||
const TestCalendarDelegate();
|
||||
|
||||
@override
|
||||
int getDaysInMonth(int year, int month) {
|
||||
return month.isEven ? 21 : 28;
|
||||
}
|
||||
|
||||
@override
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1807,6 +1807,101 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('Calendar Delegate', () {
|
||||
testWidgets('Defaults to Gregorian calendar system', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DateRangePickerDialog(
|
||||
initialDateRange: initialDateRange,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final DateRangePickerDialog dialog = tester.widget(find.byType(DateRangePickerDialog));
|
||||
expect(dialog.calendarDelegate, isA<GregorianCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Using custom calendar delegate implementation', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DateRangePickerDialog(
|
||||
initialDateRange: initialDateRange,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final DateRangePickerDialog dialog = tester.widget(find.byType(DateRangePickerDialog));
|
||||
expect(dialog.calendarDelegate, isA<TestCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Displays calendar based on the calendar delegate', (WidgetTester tester) async {
|
||||
Finder getMonthItem() {
|
||||
final Finder dayItem = find.descendant(
|
||||
of: find.byType(ConstrainedBox),
|
||||
matching: find.text('1'),
|
||||
);
|
||||
return find.ancestor(of: dayItem, matching: find.byType(Column));
|
||||
}
|
||||
|
||||
int getDayCount(Finder parent) {
|
||||
final Finder dayItem = find.descendant(
|
||||
of: parent,
|
||||
matching: find.descendant(of: find.byType(InkResponse), matching: find.byType(Text)),
|
||||
);
|
||||
return tester.widgetList(dayItem).length;
|
||||
}
|
||||
|
||||
Text getMonthYear(Finder parent) {
|
||||
return tester.widget(
|
||||
find
|
||||
.descendant(
|
||||
of: parent,
|
||||
matching: find.descendant(
|
||||
of: find.byType(ConstrainedBox),
|
||||
matching: find.byType(Text),
|
||||
),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: DateRangePickerDialog(
|
||||
initialDateRange: initialDateRange,
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder monthItem = getMonthItem();
|
||||
|
||||
final Finder firstMonthItem = monthItem.at(0);
|
||||
expect(getMonthYear(firstMonthItem).data, 'January 2016');
|
||||
expect(getDayCount(firstMonthItem), 28);
|
||||
|
||||
final Finder secondMonthItem = monthItem.at(2);
|
||||
expect(getMonthYear(secondMonthItem).data, 'February 2016');
|
||||
expect(getDayCount(secondMonthItem), 21);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _RestorableDateRangePickerDialogTestWidget extends StatefulWidget {
|
||||
@@ -1908,3 +2003,17 @@ class _RestorableDateRangePickerDialogTestWidgetState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TestCalendarDelegate extends GregorianCalendarDelegate {
|
||||
const TestCalendarDelegate();
|
||||
|
||||
@override
|
||||
int getDaysInMonth(int year, int month) {
|
||||
return month.isEven ? 21 : 28;
|
||||
}
|
||||
|
||||
@override
|
||||
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,4 +413,124 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasFocus, isFalse);
|
||||
});
|
||||
|
||||
group('Calendar Delegate', () {
|
||||
testWidgets('Defaults to Gregorian calendar system', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: InputDatePickerFormField(
|
||||
initialDate: DateTime(2025, DateTime.february, 26),
|
||||
firstDate: DateTime(2025, DateTime.february),
|
||||
lastDate: DateTime(2026, DateTime.may),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final InputDatePickerFormField inputDatePickerField = tester.widget(
|
||||
find.byType(InputDatePickerFormField),
|
||||
);
|
||||
expect(inputDatePickerField.calendarDelegate, isA<GregorianCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Using custom calendar delegate implementation', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: InputDatePickerFormField(
|
||||
initialDate: DateTime(2025, DateTime.february, 26),
|
||||
firstDate: DateTime(2025, DateTime.february),
|
||||
lastDate: DateTime(2026, DateTime.may),
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final InputDatePickerFormField inputDatePickerField = tester.widget(
|
||||
find.byType(InputDatePickerFormField),
|
||||
);
|
||||
expect(inputDatePickerField.calendarDelegate, isA<TestCalendarDelegate>());
|
||||
});
|
||||
|
||||
testWidgets('Displays calendar based on the calendar delegate', (WidgetTester tester) async {
|
||||
DateTime? selectedDate;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: Material(
|
||||
child: InputDatePickerFormField(
|
||||
initialDate: DateTime(2025, DateTime.february, 26),
|
||||
firstDate: DateTime(2025, DateTime.february),
|
||||
lastDate: DateTime(2026, DateTime.may),
|
||||
onDateSubmitted: (DateTime value) {
|
||||
selectedDate = value;
|
||||
},
|
||||
calendarDelegate: const TestCalendarDelegate(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder dateInput1 = find.descendant(
|
||||
of: find.byType(TextField),
|
||||
matching: find.text('2025..2..26'),
|
||||
);
|
||||
expect(dateInput1, findsOneWidget);
|
||||
|
||||
await tester.tap(dateInput1);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(dateInput1, '2025..3..10');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(selectedDate, DateTime(2025, DateTime.march, 10));
|
||||
|
||||
final Finder dateInput2 = find.descendant(
|
||||
of: find.byType(TextField),
|
||||
matching: find.text('2025..3..10'),
|
||||
);
|
||||
expect(dateInput2, findsOneWidget);
|
||||
|
||||
await tester.tap(dateInput2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(dateInput2, '2025..4..21');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(selectedDate, DateTime(2025, DateTime.april, 21));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestCalendarDelegate extends GregorianCalendarDelegate {
|
||||
const TestCalendarDelegate();
|
||||
|
||||
@override
|
||||
String formatCompactDate(DateTime date, MaterialLocalizations localizations) {
|
||||
return '${date.year}..${date.month}..${date.day}';
|
||||
}
|
||||
|
||||
@override
|
||||
DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) {
|
||||
final List<String> parts = inputString!.split('..');
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
final int year = int.tryParse(parts[0]) ?? 0;
|
||||
final int month = int.tryParse(parts[1]) ?? 0;
|
||||
final int day = int.tryParse(parts[2]) ?? 0;
|
||||
return DateTime(year, month, day);
|
||||
}
|
||||
|
||||
@override
|
||||
String dateHelpText(MaterialLocalizations localizations) {
|
||||
return 'yyyy..mm..dd';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user