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:
Sarbagya Dhaubanjar
2025-03-08 08:26:17 +05:45
committed by GitHub
parent 83781ae65c
commit 6d6d7914f9
10 changed files with 1095 additions and 108 deletions

View File

@@ -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;
}
}

View File

@@ -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'));
});
}

View File

@@ -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(

View File

@@ -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].
///

View File

@@ -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,
),

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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';
}
}