mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Material Date Picker code restructure (#70708)
Date Picker restructuring. Moved files out of 'pickers' and merged several of the non-public implementation files into date_picker.dart.
This commit is contained in:
parent
d4b871e1a7
commit
c5f8edd9ee
|
@ -37,6 +37,7 @@ export 'src/material/button_bar_theme.dart';
|
||||||
export 'src/material/button_style.dart';
|
export 'src/material/button_style.dart';
|
||||||
export 'src/material/button_style_button.dart';
|
export 'src/material/button_style_button.dart';
|
||||||
export 'src/material/button_theme.dart';
|
export 'src/material/button_theme.dart';
|
||||||
|
export 'src/material/calendar_date_picker.dart';
|
||||||
export 'src/material/card.dart';
|
export 'src/material/card.dart';
|
||||||
export 'src/material/card_theme.dart';
|
export 'src/material/card_theme.dart';
|
||||||
export 'src/material/checkbox.dart';
|
export 'src/material/checkbox.dart';
|
||||||
|
@ -51,6 +52,9 @@ export 'src/material/curves.dart';
|
||||||
export 'src/material/data_table.dart';
|
export 'src/material/data_table.dart';
|
||||||
export 'src/material/data_table_source.dart';
|
export 'src/material/data_table_source.dart';
|
||||||
export 'src/material/data_table_theme.dart';
|
export 'src/material/data_table_theme.dart';
|
||||||
|
export 'src/material/date.dart';
|
||||||
|
export 'src/material/date_picker.dart';
|
||||||
|
export 'src/material/date_picker_deprecated.dart';
|
||||||
export 'src/material/debug.dart';
|
export 'src/material/debug.dart';
|
||||||
export 'src/material/dialog.dart';
|
export 'src/material/dialog.dart';
|
||||||
export 'src/material/dialog_theme.dart';
|
export 'src/material/dialog_theme.dart';
|
||||||
|
@ -82,6 +86,7 @@ export 'src/material/ink_ripple.dart';
|
||||||
export 'src/material/ink_splash.dart';
|
export 'src/material/ink_splash.dart';
|
||||||
export 'src/material/ink_well.dart';
|
export 'src/material/ink_well.dart';
|
||||||
export 'src/material/input_border.dart';
|
export 'src/material/input_border.dart';
|
||||||
|
export 'src/material/input_date_picker_form_field.dart';
|
||||||
export 'src/material/input_decorator.dart';
|
export 'src/material/input_decorator.dart';
|
||||||
export 'src/material/list_tile.dart';
|
export 'src/material/list_tile.dart';
|
||||||
export 'src/material/material.dart';
|
export 'src/material/material.dart';
|
||||||
|
@ -97,7 +102,6 @@ export 'src/material/outlined_button_theme.dart';
|
||||||
export 'src/material/page.dart';
|
export 'src/material/page.dart';
|
||||||
export 'src/material/page_transitions_theme.dart';
|
export 'src/material/page_transitions_theme.dart';
|
||||||
export 'src/material/paginated_data_table.dart';
|
export 'src/material/paginated_data_table.dart';
|
||||||
export 'src/material/pickers/pickers.dart';
|
|
||||||
export 'src/material/popup_menu.dart';
|
export 'src/material/popup_menu.dart';
|
||||||
export 'src/material/popup_menu_theme.dart';
|
export 'src/material/popup_menu_theme.dart';
|
||||||
export 'src/material/progress_indicator.dart';
|
export 'src/material/progress_indicator.dart';
|
||||||
|
|
|
@ -5,22 +5,21 @@
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../color_scheme.dart';
|
import 'color_scheme.dart';
|
||||||
import '../debug.dart';
|
import 'date.dart';
|
||||||
import '../divider.dart';
|
import 'debug.dart';
|
||||||
import '../icon_button.dart';
|
import 'divider.dart';
|
||||||
import '../icons.dart';
|
import 'icon_button.dart';
|
||||||
import '../ink_well.dart';
|
import 'icons.dart';
|
||||||
import '../material_localizations.dart';
|
import 'ink_well.dart';
|
||||||
import '../text_theme.dart';
|
import 'material_localizations.dart';
|
||||||
import '../theme.dart';
|
import 'text_theme.dart';
|
||||||
|
import 'theme.dart';
|
||||||
import 'date_picker_common.dart';
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
|
|
||||||
const Duration _monthScrollDuration = Duration(milliseconds: 200);
|
const Duration _monthScrollDuration = Duration(milliseconds: 200);
|
||||||
|
|
||||||
|
@ -38,21 +37,22 @@ const double _yearPickerRowSpacing = 8.0;
|
||||||
const double _subHeaderHeight = 52.0;
|
const double _subHeaderHeight = 52.0;
|
||||||
const double _monthNavButtonsWidth = 108.0;
|
const double _monthNavButtonsWidth = 108.0;
|
||||||
|
|
||||||
/// Displays a grid of days for a given month and allows the user to select a date.
|
/// Displays a grid of days for a given month and allows the user to select a
|
||||||
|
/// date.
|
||||||
///
|
///
|
||||||
/// Days are arranged in a rectangular grid with one column for each day of the
|
/// Days are arranged in a rectangular grid with one column for each day of the
|
||||||
/// week. Controls are provided to change the year and month that the grid is
|
/// week. Controls are provided to change the year and month that the grid is
|
||||||
/// showing.
|
/// showing.
|
||||||
///
|
///
|
||||||
/// The calendar picker widget is rarely used directly. Instead, consider using
|
/// The calendar picker widget is rarely used directly. Instead, consider using
|
||||||
/// [showDatePicker], which will create a dialog that uses this as well as provides
|
/// [showDatePicker], which will create a dialog that uses this as well as
|
||||||
/// a text entry option.
|
/// provides a text entry option.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [showDatePicker], which creates a Dialog that contains a [CalendarDatePicker]
|
/// * [showDatePicker], which creates a Dialog that contains a
|
||||||
/// and provides an optional compact view where the user can enter a date as
|
/// [CalendarDatePicker] and provides an optional compact view where the
|
||||||
/// a line of text.
|
/// user can enter a date as a line of text.
|
||||||
/// * [showTimePicker], which shows a dialog that contains a material design
|
/// * [showTimePicker], which shows a dialog that contains a material design
|
||||||
/// time picker.
|
/// time picker.
|
||||||
///
|
///
|
||||||
|
@ -97,10 +97,10 @@ class CalendarDatePicker extends StatefulWidget {
|
||||||
}) : assert(initialDate != null),
|
}) : assert(initialDate != null),
|
||||||
assert(firstDate != null),
|
assert(firstDate != null),
|
||||||
assert(lastDate != null),
|
assert(lastDate != null),
|
||||||
initialDate = utils.dateOnly(initialDate),
|
initialDate = DateUtils.dateOnly(initialDate),
|
||||||
firstDate = utils.dateOnly(firstDate),
|
firstDate = DateUtils.dateOnly(firstDate),
|
||||||
lastDate = utils.dateOnly(lastDate),
|
lastDate = DateUtils.dateOnly(lastDate),
|
||||||
currentDate = utils.dateOnly(currentDate ?? DateTime.now()),
|
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
|
||||||
assert(onDateChanged != null),
|
assert(onDateChanged != null),
|
||||||
assert(initialCalendarMode != null),
|
assert(initialCalendarMode != null),
|
||||||
super(key: key) {
|
super(key: key) {
|
||||||
|
@ -152,8 +152,8 @@ class CalendarDatePicker extends StatefulWidget {
|
||||||
|
|
||||||
class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
bool _announcedInitialDate = false;
|
bool _announcedInitialDate = false;
|
||||||
DatePickerMode? _mode;
|
late DatePickerMode _mode;
|
||||||
DateTime? _currentDisplayedMonthDate;
|
late DateTime _currentDisplayedMonthDate;
|
||||||
late DateTime _selectedDate;
|
late DateTime _selectedDate;
|
||||||
final GlobalKey _monthPickerKey = GlobalKey();
|
final GlobalKey _monthPickerKey = GlobalKey();
|
||||||
final GlobalKey _yearPickerKey = GlobalKey();
|
final GlobalKey _yearPickerKey = GlobalKey();
|
||||||
|
@ -164,13 +164,20 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mode = widget.initialCalendarMode;
|
_mode = widget.initialCalendarMode;
|
||||||
_initWidgetState();
|
_currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month);
|
||||||
|
_selectedDate = widget.initialDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(CalendarDatePicker oldWidget) {
|
void didUpdateWidget(CalendarDatePicker oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
_initWidgetState();
|
if (widget.initialCalendarMode != oldWidget.initialCalendarMode) {
|
||||||
|
_mode = widget.initialCalendarMode;
|
||||||
|
}
|
||||||
|
if (!DateUtils.isSameDay(widget.initialDate, oldWidget.initialDate)) {
|
||||||
|
_currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month);
|
||||||
|
_selectedDate = widget.initialDate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -190,11 +197,6 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initWidgetState() {
|
|
||||||
_currentDisplayedMonthDate = DateTime(widget.initialDate.year, widget.initialDate.month);
|
|
||||||
_selectedDate = widget.initialDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _vibrate() {
|
void _vibrate() {
|
||||||
switch (Theme.of(context).platform) {
|
switch (Theme.of(context).platform) {
|
||||||
case TargetPlatform.android:
|
case TargetPlatform.android:
|
||||||
|
@ -229,9 +231,9 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
|
|
||||||
void _handleMonthChanged(DateTime date) {
|
void _handleMonthChanged(DateTime date) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (_currentDisplayedMonthDate!.year != date.year || _currentDisplayedMonthDate!.month != date.month) {
|
if (_currentDisplayedMonthDate.year != date.year || _currentDisplayedMonthDate.month != date.month) {
|
||||||
_currentDisplayedMonthDate = DateTime(date.year, date.month);
|
_currentDisplayedMonthDate = DateTime(date.year, date.month);
|
||||||
widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate!);
|
widget.onDisplayedMonthChanged?.call(_currentDisplayedMonthDate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -260,8 +262,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPicker() {
|
Widget _buildPicker() {
|
||||||
assert(_mode != null);
|
switch (_mode) {
|
||||||
switch (_mode!) {
|
|
||||||
case DatePickerMode.day:
|
case DatePickerMode.day:
|
||||||
return _MonthPicker(
|
return _MonthPicker(
|
||||||
key: _monthPickerKey,
|
key: _monthPickerKey,
|
||||||
|
@ -282,7 +283,7 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
currentDate: widget.currentDate,
|
currentDate: widget.currentDate,
|
||||||
firstDate: widget.firstDate,
|
firstDate: widget.firstDate,
|
||||||
lastDate: widget.lastDate,
|
lastDate: widget.lastDate,
|
||||||
initialDate: _currentDisplayedMonthDate!,
|
initialDate: _currentDisplayedMonthDate,
|
||||||
selectedDate: _selectedDate,
|
selectedDate: _selectedDate,
|
||||||
onChanged: _handleYearChanged,
|
onChanged: _handleYearChanged,
|
||||||
),
|
),
|
||||||
|
@ -303,8 +304,8 @@ class _CalendarDatePickerState extends State<CalendarDatePicker> {
|
||||||
),
|
),
|
||||||
// Put the mode toggle button on top so that it won't be covered up by the _MonthPicker
|
// Put the mode toggle button on top so that it won't be covered up by the _MonthPicker
|
||||||
_DatePickerModeToggleButton(
|
_DatePickerModeToggleButton(
|
||||||
mode: _mode!,
|
mode: _mode,
|
||||||
title: _localizations.formatMonthYear(_currentDisplayedMonthDate!),
|
title: _localizations.formatMonthYear(_currentDisplayedMonthDate),
|
||||||
onTitlePressed: () {
|
onTitlePressed: () {
|
||||||
// Toggle the day/year mode.
|
// Toggle the day/year mode.
|
||||||
_handleModeChanged(_mode == DatePickerMode.day ? DatePickerMode.year : DatePickerMode.day);
|
_handleModeChanged(_mode == DatePickerMode.day ? DatePickerMode.year : DatePickerMode.day);
|
||||||
|
@ -452,7 +453,7 @@ class _MonthPicker extends StatefulWidget {
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The initial month to display.
|
/// The initial month to display.
|
||||||
final DateTime? initialMonth;
|
final DateTime initialMonth;
|
||||||
|
|
||||||
/// The current date.
|
/// The current date.
|
||||||
///
|
///
|
||||||
|
@ -489,24 +490,24 @@ class _MonthPicker extends StatefulWidget {
|
||||||
|
|
||||||
class _MonthPickerState extends State<_MonthPicker> {
|
class _MonthPickerState extends State<_MonthPicker> {
|
||||||
final GlobalKey _pageViewKey = GlobalKey();
|
final GlobalKey _pageViewKey = GlobalKey();
|
||||||
DateTime? _currentMonth;
|
late DateTime _currentMonth;
|
||||||
late DateTime _nextMonthDate;
|
late DateTime _nextMonthDate;
|
||||||
late DateTime _previousMonthDate;
|
late DateTime _previousMonthDate;
|
||||||
PageController? _pageController;
|
late PageController _pageController;
|
||||||
late MaterialLocalizations _localizations;
|
late MaterialLocalizations _localizations;
|
||||||
late TextDirection _textDirection;
|
late TextDirection _textDirection;
|
||||||
Map<LogicalKeySet, Intent>? _shortcutMap;
|
Map<LogicalKeySet, Intent>? _shortcutMap;
|
||||||
Map<Type, Action<Intent>>? _actionMap;
|
Map<Type, Action<Intent>>? _actionMap;
|
||||||
FocusNode? _dayGridFocus;
|
late FocusNode _dayGridFocus;
|
||||||
DateTime? _focusedDay;
|
DateTime? _focusedDay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_currentMonth = widget.initialMonth;
|
_currentMonth = widget.initialMonth;
|
||||||
_previousMonthDate = utils.addMonthsToMonthDate(_currentMonth!, -1);
|
_previousMonthDate = DateUtils.addMonthsToMonthDate(_currentMonth, -1);
|
||||||
_nextMonthDate = utils.addMonthsToMonthDate(_currentMonth!, 1);
|
_nextMonthDate = DateUtils.addMonthsToMonthDate(_currentMonth, 1);
|
||||||
_pageController = PageController(initialPage: utils.monthDelta(widget.firstDate, _currentMonth!));
|
_pageController = PageController(initialPage: DateUtils.monthDelta(widget.firstDate, _currentMonth));
|
||||||
_shortcutMap = <LogicalKeySet, Intent>{
|
_shortcutMap = <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||||
|
@ -532,14 +533,17 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
void didUpdateWidget(_MonthPicker oldWidget) {
|
void didUpdateWidget(_MonthPicker oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.initialMonth != oldWidget.initialMonth) {
|
if (widget.initialMonth != oldWidget.initialMonth) {
|
||||||
_showMonth(widget.initialMonth!);
|
// We can't interrupt this widget build with a scroll, so do it next frame
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback(
|
||||||
|
(Duration timeStamp) => _showMonth(widget.initialMonth, jump: true)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pageController?.dispose();
|
_pageController.dispose();
|
||||||
_dayGridFocus!.dispose();
|
_dayGridFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,17 +554,17 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
|
|
||||||
void _handleMonthPageChanged(int monthPage) {
|
void _handleMonthPageChanged(int monthPage) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final DateTime monthDate = utils.addMonthsToMonthDate(widget.firstDate, monthPage);
|
final DateTime monthDate = DateUtils.addMonthsToMonthDate(widget.firstDate, monthPage);
|
||||||
if (!utils.isSameMonth(_currentMonth, monthDate)) {
|
if (!DateUtils.isSameMonth(_currentMonth, monthDate)) {
|
||||||
_currentMonth = DateTime(monthDate.year, monthDate.month);
|
_currentMonth = DateTime(monthDate.year, monthDate.month);
|
||||||
_previousMonthDate = utils.addMonthsToMonthDate(_currentMonth!, -1);
|
_previousMonthDate = DateUtils.addMonthsToMonthDate(_currentMonth, -1);
|
||||||
_nextMonthDate = utils.addMonthsToMonthDate(_currentMonth!, 1);
|
_nextMonthDate = DateUtils.addMonthsToMonthDate(_currentMonth, 1);
|
||||||
widget.onDisplayedMonthChanged(_currentMonth!);
|
widget.onDisplayedMonthChanged(_currentMonth);
|
||||||
if (_focusedDay != null && !utils.isSameMonth(_focusedDay, _currentMonth)) {
|
if (_focusedDay != null && !DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
|
||||||
// We have navigated to a new month with the grid focused, but the
|
// 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
|
// focused day is not in this month. Choose a new one trying to keep
|
||||||
// the same day of the month.
|
// the same day of the month.
|
||||||
_focusedDay = _focusableDayForMonth(_currentMonth!, _focusedDay!.day);
|
_focusedDay = _focusableDayForMonth(_currentMonth, _focusedDay!.day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -572,7 +576,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
/// otherwise the first selectable day in the month will be returned. If
|
/// otherwise the first selectable day in the month will be returned. If
|
||||||
/// no dates are selectable in the month, then it will return null.
|
/// no dates are selectable in the month, then it will return null.
|
||||||
DateTime? _focusableDayForMonth(DateTime month, int preferredDay) {
|
DateTime? _focusableDayForMonth(DateTime month, int preferredDay) {
|
||||||
final int daysInMonth = utils.getDaysInMonth(month.year, month.month);
|
final int daysInMonth = DateUtils.getDaysInMonth(month.year, month.month);
|
||||||
|
|
||||||
// Can we use the preferred day in this month?
|
// Can we use the preferred day in this month?
|
||||||
if (preferredDay <= daysInMonth) {
|
if (preferredDay <= daysInMonth) {
|
||||||
|
@ -597,7 +601,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
_localizations.formatMonthYear(_nextMonthDate),
|
_localizations.formatMonthYear(_nextMonthDate),
|
||||||
_textDirection,
|
_textDirection,
|
||||||
);
|
);
|
||||||
_pageController!.nextPage(
|
_pageController.nextPage(
|
||||||
duration: _monthScrollDuration,
|
duration: _monthScrollDuration,
|
||||||
curve: Curves.ease,
|
curve: Curves.ease,
|
||||||
);
|
);
|
||||||
|
@ -611,7 +615,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
_localizations.formatMonthYear(_previousMonthDate),
|
_localizations.formatMonthYear(_previousMonthDate),
|
||||||
_textDirection,
|
_textDirection,
|
||||||
);
|
);
|
||||||
_pageController!.previousPage(
|
_pageController.previousPage(
|
||||||
duration: _monthScrollDuration,
|
duration: _monthScrollDuration,
|
||||||
curve: Curves.ease,
|
curve: Curves.ease,
|
||||||
);
|
);
|
||||||
|
@ -619,24 +623,28 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate to the given month.
|
/// Navigate to the given month.
|
||||||
void _showMonth(DateTime month) {
|
void _showMonth(DateTime month, { bool jump = false}) {
|
||||||
final int monthPage = utils.monthDelta(widget.firstDate, month);
|
final int monthPage = DateUtils.monthDelta(widget.firstDate, month);
|
||||||
_pageController!.animateToPage(monthPage,
|
if (jump) {
|
||||||
duration: _monthScrollDuration,
|
_pageController.jumpToPage(monthPage);
|
||||||
curve: Curves.ease
|
} else {
|
||||||
);
|
_pageController.animateToPage(monthPage,
|
||||||
|
duration: _monthScrollDuration,
|
||||||
|
curve: Curves.ease
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the earliest allowable month is displayed.
|
/// True if the earliest allowable month is displayed.
|
||||||
bool get _isDisplayingFirstMonth {
|
bool get _isDisplayingFirstMonth {
|
||||||
return !_currentMonth!.isAfter(
|
return !_currentMonth.isAfter(
|
||||||
DateTime(widget.firstDate.year, widget.firstDate.month),
|
DateTime(widget.firstDate.year, widget.firstDate.month),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the latest allowable month is displayed.
|
/// True if the latest allowable month is displayed.
|
||||||
bool get _isDisplayingLastMonth {
|
bool get _isDisplayingLastMonth {
|
||||||
return !_currentMonth!.isBefore(
|
return !_currentMonth.isBefore(
|
||||||
DateTime(widget.lastDate.year, widget.lastDate.month),
|
DateTime(widget.lastDate.year, widget.lastDate.month),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -645,12 +653,12 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
void _handleGridFocusChange(bool focused) {
|
void _handleGridFocusChange(bool focused) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (focused && _focusedDay == null) {
|
if (focused && _focusedDay == null) {
|
||||||
if (utils.isSameMonth(widget.selectedDate, _currentMonth)) {
|
if (DateUtils.isSameMonth(widget.selectedDate, _currentMonth)) {
|
||||||
_focusedDay = widget.selectedDate;
|
_focusedDay = widget.selectedDate;
|
||||||
} else if (utils.isSameMonth(widget.currentDate, _currentMonth)) {
|
} else if (DateUtils.isSameMonth(widget.currentDate, _currentMonth)) {
|
||||||
_focusedDay = _focusableDayForMonth(_currentMonth!, widget.currentDate.day);
|
_focusedDay = _focusableDayForMonth(_currentMonth, widget.currentDate.day);
|
||||||
} else {
|
} else {
|
||||||
_focusedDay = _focusableDayForMonth(_currentMonth!, 1);
|
_focusedDay = _focusableDayForMonth(_currentMonth, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -658,14 +666,14 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
|
|
||||||
/// Move focus to the next element after the day grid.
|
/// Move focus to the next element after the day grid.
|
||||||
void _handleGridNextFocus(NextFocusIntent intent) {
|
void _handleGridNextFocus(NextFocusIntent intent) {
|
||||||
_dayGridFocus!.requestFocus();
|
_dayGridFocus.requestFocus();
|
||||||
_dayGridFocus!.nextFocus();
|
_dayGridFocus.nextFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move focus to the previous element before the day grid.
|
/// Move focus to the previous element before the day grid.
|
||||||
void _handleGridPreviousFocus(PreviousFocusIntent intent) {
|
void _handleGridPreviousFocus(PreviousFocusIntent intent) {
|
||||||
_dayGridFocus!.requestFocus();
|
_dayGridFocus.requestFocus();
|
||||||
_dayGridFocus!.previousFocus();
|
_dayGridFocus.previousFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move the internal focus date in the direction of the given intent.
|
/// Move the internal focus date in the direction of the given intent.
|
||||||
|
@ -683,7 +691,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
final DateTime? nextDate = _nextDateInDirection(_focusedDay!, intent.direction);
|
final DateTime? nextDate = _nextDateInDirection(_focusedDay!, intent.direction);
|
||||||
if (nextDate != null) {
|
if (nextDate != null) {
|
||||||
_focusedDay = nextDate;
|
_focusedDay = nextDate;
|
||||||
if (!utils.isSameMonth(_focusedDay, _currentMonth)) {
|
if (!DateUtils.isSameMonth(_focusedDay, _currentMonth)) {
|
||||||
_showMonth(_focusedDay!);
|
_showMonth(_focusedDay!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -710,12 +718,12 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
|
|
||||||
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
|
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
final TextDirection textDirection = Directionality.of(context);
|
||||||
DateTime nextDate = utils.addDaysToDate(date, _dayDirectionOffset(direction, textDirection));
|
DateTime nextDate = DateUtils.addDaysToDate(date, _dayDirectionOffset(direction, textDirection));
|
||||||
while (!nextDate.isBefore(widget.firstDate) && !nextDate.isAfter(widget.lastDate)) {
|
while (!nextDate.isBefore(widget.firstDate) && !nextDate.isAfter(widget.lastDate)) {
|
||||||
if (_isSelectable(nextDate)) {
|
if (_isSelectable(nextDate)) {
|
||||||
return nextDate;
|
return nextDate;
|
||||||
}
|
}
|
||||||
nextDate = utils.addDaysToDate(nextDate, _dayDirectionOffset(direction, textDirection));
|
nextDate = DateUtils.addDaysToDate(nextDate, _dayDirectionOffset(direction, textDirection));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -725,7 +733,7 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildItems(BuildContext context, int index) {
|
Widget _buildItems(BuildContext context, int index) {
|
||||||
final DateTime month = utils.addMonthsToMonthDate(widget.firstDate, index);
|
final DateTime month = DateUtils.addMonthsToMonthDate(widget.firstDate, index);
|
||||||
return _DayPicker(
|
return _DayPicker(
|
||||||
key: ValueKey<DateTime>(month),
|
key: ValueKey<DateTime>(month),
|
||||||
selectedDate: widget.selectedDate,
|
selectedDate: widget.selectedDate,
|
||||||
|
@ -775,12 +783,12 @@ class _MonthPickerState extends State<_MonthPicker> {
|
||||||
focusNode: _dayGridFocus,
|
focusNode: _dayGridFocus,
|
||||||
onFocusChange: _handleGridFocusChange,
|
onFocusChange: _handleGridFocusChange,
|
||||||
child: _FocusedDate(
|
child: _FocusedDate(
|
||||||
date: _dayGridFocus!.hasFocus ? _focusedDay : null,
|
date: _dayGridFocus.hasFocus ? _focusedDay : null,
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
key: _pageViewKey,
|
key: _pageViewKey,
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
itemBuilder: _buildItems,
|
itemBuilder: _buildItems,
|
||||||
itemCount: utils.monthDelta(widget.firstDate, widget.lastDate) + 1,
|
itemCount: DateUtils.monthDelta(widget.firstDate, widget.lastDate) + 1,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: _handleMonthPageChanged,
|
onPageChanged: _handleMonthPageChanged,
|
||||||
),
|
),
|
||||||
|
@ -808,7 +816,7 @@ class _FocusedDate extends InheritedWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(_FocusedDate oldWidget) {
|
bool updateShouldNotify(_FocusedDate oldWidget) {
|
||||||
return !utils.isSameDay(date, oldWidget.date);
|
return !DateUtils.isSameDay(date, oldWidget.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
static DateTime? of(BuildContext context) {
|
static DateTime? of(BuildContext context) {
|
||||||
|
@ -882,7 +890,7 @@ class _DayPickerState extends State<_DayPicker> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final int daysInMonth = utils.getDaysInMonth(widget.displayedMonth.year, widget.displayedMonth.month);
|
final int daysInMonth = DateUtils.getDaysInMonth(widget.displayedMonth.year, widget.displayedMonth.month);
|
||||||
_dayFocusNodes = List<FocusNode>.generate(
|
_dayFocusNodes = List<FocusNode>.generate(
|
||||||
daysInMonth,
|
daysInMonth,
|
||||||
(int index) => FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}')
|
(int index) => FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}')
|
||||||
|
@ -894,7 +902,7 @@ class _DayPickerState extends State<_DayPicker> {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
// Check to see if the focused date is in this month, if so focus it.
|
// Check to see if the focused date is in this month, if so focus it.
|
||||||
final DateTime? focusedDate = _FocusedDate.of(context);
|
final DateTime? focusedDate = _FocusedDate.of(context);
|
||||||
if (focusedDate != null && utils.isSameMonth(widget.displayedMonth, focusedDate)) {
|
if (focusedDate != null && DateUtils.isSameMonth(widget.displayedMonth, focusedDate)) {
|
||||||
_dayFocusNodes[focusedDate.day - 1].requestFocus();
|
_dayFocusNodes[focusedDate.day - 1].requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -956,8 +964,8 @@ class _DayPickerState extends State<_DayPicker> {
|
||||||
final int year = widget.displayedMonth.year;
|
final int year = widget.displayedMonth.year;
|
||||||
final int month = widget.displayedMonth.month;
|
final int month = widget.displayedMonth.month;
|
||||||
|
|
||||||
final int daysInMonth = utils.getDaysInMonth(year, month);
|
final int daysInMonth = DateUtils.getDaysInMonth(year, month);
|
||||||
final int dayOffset = utils.firstDayOffset(year, month, localizations);
|
final int dayOffset = DateUtils.firstDayOffset(year, month, localizations);
|
||||||
|
|
||||||
final List<Widget> dayItems = _dayHeaders(headerStyle, localizations);
|
final List<Widget> dayItems = _dayHeaders(headerStyle, localizations);
|
||||||
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
|
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
|
||||||
|
@ -972,8 +980,8 @@ class _DayPickerState extends State<_DayPicker> {
|
||||||
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
|
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) ||
|
||||||
dayToBuild.isBefore(widget.firstDate) ||
|
dayToBuild.isBefore(widget.firstDate) ||
|
||||||
(widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild));
|
(widget.selectableDayPredicate != null && !widget.selectableDayPredicate!(dayToBuild));
|
||||||
final bool isSelectedDay = utils.isSameDay(widget.selectedDate, dayToBuild);
|
final bool isSelectedDay = DateUtils.isSameDay(widget.selectedDate, dayToBuild);
|
||||||
final bool isToday = utils.isSameDay(widget.currentDate, dayToBuild);
|
final bool isToday = DateUtils.isSameDay(widget.currentDate, dayToBuild);
|
||||||
|
|
||||||
BoxDecoration? decoration;
|
BoxDecoration? decoration;
|
||||||
Color dayColor = enabledDayColor;
|
Color dayColor = enabledDayColor;
|
||||||
|
@ -1074,27 +1082,40 @@ class _DayPickerGridDelegate extends SliverGridDelegate {
|
||||||
|
|
||||||
const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate();
|
const _DayPickerGridDelegate _dayPickerGridDelegate = _DayPickerGridDelegate();
|
||||||
|
|
||||||
/// A scrollable list of years to allow picking a year.
|
/// A scrollable grid of years to allow picking a year.
|
||||||
|
///
|
||||||
|
/// The year picker widget is rarely used directly. Instead, consider using
|
||||||
|
/// [CalendarDatePicker], or [showDatePicker] which create full date pickers.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [CalendarDatePicker], which provides a Material Design date picker
|
||||||
|
/// interface.
|
||||||
|
///
|
||||||
|
/// * [showDatePicker], which shows a dialog containing a Material Design
|
||||||
|
/// date picker.
|
||||||
|
///
|
||||||
class _YearPicker extends StatefulWidget {
|
class _YearPicker extends StatefulWidget {
|
||||||
/// Creates a year picker.
|
/// Creates a year picker.
|
||||||
///
|
///
|
||||||
/// The [currentDate, [firstDate], [lastDate], [selectedDate], and [onChanged]
|
/// The [firstDate], [lastDate], [selectedDate], and [onChanged]
|
||||||
/// arguments must be non-null. The [lastDate] must be after the [firstDate].
|
/// arguments must be non-null. The [lastDate] must be after the [firstDate].
|
||||||
_YearPicker({
|
_YearPicker({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.currentDate,
|
DateTime? currentDate,
|
||||||
required this.firstDate,
|
required this.firstDate,
|
||||||
required this.lastDate,
|
required this.lastDate,
|
||||||
required this.initialDate,
|
DateTime? initialDate,
|
||||||
required this.selectedDate,
|
required this.selectedDate,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
}) : assert(currentDate != null),
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
assert(firstDate != null),
|
}) : assert(firstDate != null),
|
||||||
assert(lastDate != null),
|
assert(lastDate != null),
|
||||||
assert(initialDate != null),
|
|
||||||
assert(selectedDate != null),
|
assert(selectedDate != null),
|
||||||
assert(onChanged != null),
|
assert(onChanged != null),
|
||||||
assert(!firstDate.isAfter(lastDate)),
|
assert(!firstDate.isAfter(lastDate)),
|
||||||
|
currentDate = DateUtils.dateOnly(currentDate ?? DateTime.now()),
|
||||||
|
initialDate = DateUtils.dateOnly(initialDate ?? selectedDate),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// The current date.
|
/// The current date.
|
||||||
|
@ -1119,12 +1140,15 @@ class _YearPicker extends StatefulWidget {
|
||||||
/// Called when the user picks a year.
|
/// Called when the user picks a year.
|
||||||
final ValueChanged<DateTime> onChanged;
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_YearPickerState createState() => _YearPickerState();
|
_YearPickerState createState() => _YearPickerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _YearPickerState extends State<_YearPicker> {
|
class _YearPickerState extends State<_YearPicker> {
|
||||||
late ScrollController scrollController;
|
late ScrollController _scrollController;
|
||||||
|
|
||||||
// The approximate number of years necessary to fill the available space.
|
// The approximate number of years necessary to fill the available space.
|
||||||
static const int minYears = 18;
|
static const int minYears = 18;
|
||||||
|
@ -1132,14 +1156,23 @@ class _YearPickerState extends State<_YearPicker> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_scrollController = ScrollController(initialScrollOffset: _scrollOffsetForYear(widget.selectedDate));
|
||||||
|
}
|
||||||
|
|
||||||
// Set the scroll position to approximately center the initial year.
|
@override
|
||||||
final int initialYearIndex = widget.selectedDate.year - widget.firstDate.year;
|
void didUpdateWidget(_YearPicker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.selectedDate != oldWidget.selectedDate) {
|
||||||
|
_scrollController.jumpTo(_scrollOffsetForYear(widget.selectedDate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _scrollOffsetForYear(DateTime date) {
|
||||||
|
final int initialYearIndex = date.year - widget.firstDate.year;
|
||||||
final int initialYearRow = initialYearIndex ~/ _yearPickerColumnCount;
|
final int initialYearRow = initialYearIndex ~/ _yearPickerColumnCount;
|
||||||
// Move the offset down by 2 rows to approximately center it.
|
// Move the offset down by 2 rows to approximately center it.
|
||||||
final int centeredYearRow = initialYearRow - 2;
|
final int centeredYearRow = initialYearRow - 2;
|
||||||
final double scrollOffset = _itemCount < minYears ? 0 : centeredYearRow * _yearPickerRowHeight;
|
return _itemCount < minYears ? 0 : centeredYearRow * _yearPickerRowHeight;
|
||||||
scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildYearItem(BuildContext context, int index) {
|
Widget _buildYearItem(BuildContext context, int index) {
|
||||||
|
@ -1206,15 +1239,7 @@ class _YearPickerState extends State<_YearPicker> {
|
||||||
} else {
|
} else {
|
||||||
yearItem = InkWell(
|
yearItem = InkWell(
|
||||||
key: ValueKey<int>(year),
|
key: ValueKey<int>(year),
|
||||||
onTap: () {
|
onTap: () => widget.onChanged(DateTime(year, widget.initialDate.month, 1)),
|
||||||
widget.onChanged(
|
|
||||||
DateTime(
|
|
||||||
year,
|
|
||||||
widget.initialDate.month,
|
|
||||||
widget.initialDate.day,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: yearItem,
|
child: yearItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1228,12 +1253,14 @@ class _YearPickerState extends State<_YearPicker> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
assert(debugCheckHasMaterial(context));
|
||||||
return Column(
|
return Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GridView.builder(
|
child: GridView.builder(
|
||||||
controller: scrollController,
|
controller: _scrollController,
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
gridDelegate: _yearPickerGridDelegate,
|
gridDelegate: _yearPickerGridDelegate,
|
||||||
itemBuilder: _buildYearItem,
|
itemBuilder: _buildYearItem,
|
||||||
itemCount: math.max(_itemCount, minYears),
|
itemCount: math.max(_itemCount, minYears),
|
232
packages/flutter/lib/src/material/date.dart
Normal file
232
packages/flutter/lib/src/material/date.dart
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' show hashValues;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'material_localizations.dart';
|
||||||
|
|
||||||
|
/// Utility functions for working with dates.
|
||||||
|
class DateUtils {
|
||||||
|
// This class is not meant to be instantiated or extended; this constructor
|
||||||
|
// prevents instantiation and extension.
|
||||||
|
DateUtils._();
|
||||||
|
|
||||||
|
/// Returns a [DateTime] with the date of the original, but time set to
|
||||||
|
/// midnight.
|
||||||
|
static DateTime dateOnly(DateTime date) {
|
||||||
|
return DateTime(date.year, date.month, date.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
static DateTimeRange datesOnly(DateTimeRange range) {
|
||||||
|
return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the two [DateTime] objects have the same day, month, and
|
||||||
|
/// year, or are both null.
|
||||||
|
static bool isSameDay(DateTime? dateA, DateTime? dateB) {
|
||||||
|
return
|
||||||
|
dateA?.year == dateB?.year &&
|
||||||
|
dateA?.month == dateB?.month &&
|
||||||
|
dateA?.day == dateB?.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the two [DateTime] objects have the same month and
|
||||||
|
/// year, or are both null.
|
||||||
|
static bool isSameMonth(DateTime? dateA, DateTime? dateB) {
|
||||||
|
return
|
||||||
|
dateA?.year == dateB?.year &&
|
||||||
|
dateA?.month == dateB?.month;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the number of months between two [DateTime] objects.
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
/// ```
|
||||||
|
/// DateTime date1 = DateTime(year: 2019, month: 6, day: 15);
|
||||||
|
/// DateTime date2 = DateTime(year: 2020, month: 1, day: 15);
|
||||||
|
/// int delta = monthDelta(date1, date2);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The value for `delta` would be `7`.
|
||||||
|
static int monthDelta(DateTime startDate, DateTime endDate) {
|
||||||
|
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [DateTime] that is [monthDate] with the added number
|
||||||
|
/// of months and the day set to 1 and time set to midnight.
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
/// ```
|
||||||
|
/// DateTime date = DateTime(year: 2019, month: 1, day: 15);
|
||||||
|
/// DateTime futureDate = DateUtils.addMonthsToMonthDate(date, 3);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// `date` would be January 15, 2019.
|
||||||
|
/// `futureDate` would be April 1, 2019 since it adds 3 months.
|
||||||
|
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
|
||||||
|
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [DateTime] with the added number of days and time set to
|
||||||
|
/// midnight.
|
||||||
|
static DateTime addDaysToDate(DateTime date, int days) {
|
||||||
|
return DateTime(date.year, date.month, date.day + days);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the offset from the first day of the week that the first day of
|
||||||
|
/// the [month] falls on.
|
||||||
|
///
|
||||||
|
/// For example, September 1, 2017 falls on a Friday, which in the calendar
|
||||||
|
/// localized for United States English appears as:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// S M T W T F S
|
||||||
|
/// _ _ _ _ _ 1 2
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The offset for the first day of the months is the number of leading blanks
|
||||||
|
/// in the calendar, i.e. 5.
|
||||||
|
///
|
||||||
|
/// The same date localized for the Russian calendar has a different offset,
|
||||||
|
/// because the first day of week is Monday rather than Sunday:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// M T W T F S S
|
||||||
|
/// _ _ _ _ 1 2 3
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// So the offset is 4, rather than 5.
|
||||||
|
///
|
||||||
|
/// This code consolidates the following:
|
||||||
|
///
|
||||||
|
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
|
||||||
|
/// falling on Monday.
|
||||||
|
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
|
||||||
|
/// into the [MaterialLocalizations.narrowWeekdays] list.
|
||||||
|
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
|
||||||
|
/// days of week, always starting with Sunday and ending with Saturday.
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 0-based start of week depending on the locale, with 0 representing Sunday.
|
||||||
|
int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex;
|
||||||
|
|
||||||
|
// firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with
|
||||||
|
// weekdayFromMonday.
|
||||||
|
firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7;
|
||||||
|
|
||||||
|
// Number of days between the first day of week appearing on the calendar,
|
||||||
|
// and the day corresponding to the first of the month.
|
||||||
|
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
static int getDaysInMonth(int year, int month) {
|
||||||
|
if (month == DateTime.february) {
|
||||||
|
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
|
||||||
|
return isLeapYear ? 29 : 28;
|
||||||
|
}
|
||||||
|
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
|
return daysInMonth[month - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mode of the date picker dialog.
|
||||||
|
///
|
||||||
|
/// Either a calendar or text input. In [calendar] mode, a calendar view is
|
||||||
|
/// displayed and the user taps the day they wish to select. In [input] mode a
|
||||||
|
/// [TextField] is displayed and the user types in the date they wish to select.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [showDatePicker] and [showDateRangePicker], which use this to control
|
||||||
|
/// the initial entry mode of their dialogs.
|
||||||
|
enum DatePickerEntryMode {
|
||||||
|
/// Tapping on a calendar.
|
||||||
|
calendar,
|
||||||
|
|
||||||
|
/// Text input.
|
||||||
|
input,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initial display of a calendar date picker.
|
||||||
|
///
|
||||||
|
/// Either a grid of available years or a monthly calendar.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [showDatePicker], which shows a dialog that contains a material design
|
||||||
|
/// date picker.
|
||||||
|
/// * [CalendarDatePicker], widget which implements the material design date picker.
|
||||||
|
enum DatePickerMode {
|
||||||
|
/// Choosing a month and day.
|
||||||
|
day,
|
||||||
|
|
||||||
|
/// Choosing a year.
|
||||||
|
year,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signature for predicating dates for enabled date selections.
|
||||||
|
///
|
||||||
|
/// See [showDatePicker], which has a [SelectableDayPredicate] parameter used
|
||||||
|
/// to specify allowable days in the date picker.
|
||||||
|
typedef SelectableDayPredicate = bool Function(DateTime day);
|
||||||
|
|
||||||
|
/// Encapsulates a start and end [DateTime] that represent the range of dates.
|
||||||
|
///
|
||||||
|
/// The range includes the [start] and [end] dates. The [start] and [end] dates
|
||||||
|
/// may be equal to indicate a date range of a single day. The [start] date must
|
||||||
|
/// not be after the [end] date.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [showDateRangePicker], which displays a dialog that allows the user to
|
||||||
|
/// select a date range.
|
||||||
|
@immutable
|
||||||
|
class DateTimeRange {
|
||||||
|
/// Creates a date range for the given start and end [DateTime].
|
||||||
|
DateTimeRange({
|
||||||
|
required this.start,
|
||||||
|
required this.end,
|
||||||
|
}) : assert(start != null),
|
||||||
|
assert(end != null),
|
||||||
|
assert(!start.isAfter(end));
|
||||||
|
|
||||||
|
/// The start of the range of dates.
|
||||||
|
final DateTime start;
|
||||||
|
|
||||||
|
/// The end of the range of dates.
|
||||||
|
final DateTime end;
|
||||||
|
|
||||||
|
/// Returns a [Duration] of the time between [start] and [end].
|
||||||
|
///
|
||||||
|
/// See [DateTime.difference] for more details.
|
||||||
|
Duration get duration => end.difference(start);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType)
|
||||||
|
return false;
|
||||||
|
return other is DateTimeRange
|
||||||
|
&& other.start == start
|
||||||
|
&& other.end == end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => hashValues(start, end);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '$start - $end';
|
||||||
|
}
|
2631
packages/flutter/lib/src/material/date_picker.dart
Normal file
2631
packages/flutter/lib/src/material/date_picker.dart
Normal file
File diff suppressed because it is too large
Load diff
|
@ -9,15 +9,14 @@ import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../debug.dart';
|
import 'date.dart';
|
||||||
import '../icon_button.dart';
|
import 'debug.dart';
|
||||||
import '../icons.dart';
|
import 'icon_button.dart';
|
||||||
import '../ink_well.dart';
|
import 'icons.dart';
|
||||||
import '../material.dart';
|
import 'ink_well.dart';
|
||||||
import '../material_localizations.dart';
|
import 'material.dart';
|
||||||
import '../theme.dart';
|
import 'material_localizations.dart';
|
||||||
|
import 'theme.dart';
|
||||||
import 'date_picker_common.dart';
|
|
||||||
|
|
||||||
// This is the original implementation for the Material Date Picker.
|
// This is the original implementation for the Material Date Picker.
|
||||||
// These classes are deprecated and the whole file can be removed after
|
// These classes are deprecated and the whole file can be removed after
|
|
@ -5,14 +5,12 @@
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import '../input_border.dart';
|
import 'date.dart';
|
||||||
import '../input_decorator.dart';
|
import 'input_border.dart';
|
||||||
import '../material_localizations.dart';
|
import 'input_decorator.dart';
|
||||||
import '../text_form_field.dart';
|
import 'material_localizations.dart';
|
||||||
import '../theme.dart';
|
import 'text_form_field.dart';
|
||||||
|
import 'theme.dart';
|
||||||
import 'date_picker_common.dart';
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
|
|
||||||
/// A [TextFormField] configured to accept and validate a date entered by a user.
|
/// A [TextFormField] configured to accept and validate a date entered by a user.
|
||||||
///
|
///
|
||||||
|
@ -63,9 +61,9 @@ class InputDatePickerFormField extends StatefulWidget {
|
||||||
}) : assert(firstDate != null),
|
}) : assert(firstDate != null),
|
||||||
assert(lastDate != null),
|
assert(lastDate != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
|
initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null,
|
||||||
firstDate = utils.dateOnly(firstDate),
|
firstDate = DateUtils.dateOnly(firstDate),
|
||||||
lastDate = utils.dateOnly(lastDate),
|
lastDate = DateUtils.dateOnly(lastDate),
|
||||||
super(key: key) {
|
super(key: key) {
|
||||||
assert(
|
assert(
|
||||||
!this.lastDate.isBefore(this.firstDate),
|
!this.lastDate.isBefore(this.firstDate),
|
||||||
|
@ -156,6 +154,24 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
|
_updateValueForSelectedDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(InputDatePickerFormField oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.initialDate != oldWidget.initialDate) {
|
||||||
|
// Can't update the form field in the middle of a build, so do it next frame
|
||||||
|
WidgetsBinding.instance!.addPostFrameCallback((Duration timeStamp) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDate = widget.initialDate;
|
||||||
|
_updateValueForSelectedDate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateValueForSelectedDate() {
|
||||||
if (_selectedDate != null) {
|
if (_selectedDate != null) {
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
_inputText = localizations.formatCompactDate(_selectedDate!);
|
_inputText = localizations.formatCompactDate(_selectedDate!);
|
||||||
|
@ -169,6 +185,9 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||||
_autoSelected = true;
|
_autoSelected = true;
|
||||||
}
|
}
|
||||||
_controller.value = textEditingValue;
|
_controller.value = textEditingValue;
|
||||||
|
} else {
|
||||||
|
_inputText = '';
|
||||||
|
_controller.value = _controller.value.copyWith(text: _inputText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,26 +214,21 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSaved(String? text) {
|
void _updateDate(String? text, ValueChanged<DateTime>? callback) {
|
||||||
if (widget.onDateSaved != null) {
|
final DateTime? date = _parseDate(text);
|
||||||
final DateTime? date = _parseDate(text);
|
if (_isValidAcceptableDate(date)) {
|
||||||
if (_isValidAcceptableDate(date)) {
|
_selectedDate = date;
|
||||||
_selectedDate = date;
|
_inputText = text;
|
||||||
_inputText = text;
|
callback?.call(_selectedDate!);
|
||||||
widget.onDateSaved!(date!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleSaved(String? text) {
|
||||||
|
_updateDate(text, widget.onDateSaved);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleSubmitted(String text) {
|
void _handleSubmitted(String text) {
|
||||||
if (widget.onDateSubmitted != null) {
|
_updateDate(text, widget.onDateSubmitted);
|
||||||
final DateTime? date = _parseDate(text);
|
|
||||||
if (_isValidAcceptableDate(date)) {
|
|
||||||
_selectedDate = date;
|
|
||||||
_inputText = text;
|
|
||||||
widget.onDateSubmitted!(date!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
|
@ -1,999 +0,0 @@
|
||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import '../color_scheme.dart';
|
|
||||||
import '../divider.dart';
|
|
||||||
import '../ink_well.dart';
|
|
||||||
import '../material_localizations.dart';
|
|
||||||
import '../text_theme.dart';
|
|
||||||
import '../theme.dart';
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
|
|
||||||
const Duration _monthScrollDuration = Duration(milliseconds: 200);
|
|
||||||
|
|
||||||
const double _monthItemHeaderHeight = 58.0;
|
|
||||||
const double _monthItemFooterHeight = 12.0;
|
|
||||||
const double _monthItemRowHeight = 42.0;
|
|
||||||
const double _monthItemSpaceBetweenRows = 8.0;
|
|
||||||
const double _horizontalPadding = 8.0;
|
|
||||||
const double _maxCalendarWidthLandscape = 384.0;
|
|
||||||
const double _maxCalendarWidthPortrait = 480.0;
|
|
||||||
|
|
||||||
/// Displays a scrollable calendar grid that allows a user to select a range
|
|
||||||
/// of dates.
|
|
||||||
//
|
|
||||||
// This is not publicly exported (see pickers.dart), as it is an
|
|
||||||
// internal component used by [showDateRangePicker].
|
|
||||||
class CalendarDateRangePicker extends StatefulWidget {
|
|
||||||
/// Creates a scrollable calendar grid for picking date ranges.
|
|
||||||
CalendarDateRangePicker({
|
|
||||||
Key? key,
|
|
||||||
DateTime? initialStartDate,
|
|
||||||
DateTime? initialEndDate,
|
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
DateTime? currentDate,
|
|
||||||
required this.onStartDateChanged,
|
|
||||||
required this.onEndDateChanged,
|
|
||||||
}) : initialStartDate = initialStartDate != null ? utils.dateOnly(initialStartDate) : null,
|
|
||||||
initialEndDate = initialEndDate != null ? utils.dateOnly(initialEndDate) : null,
|
|
||||||
assert(firstDate != null),
|
|
||||||
assert(lastDate != null),
|
|
||||||
firstDate = utils.dateOnly(firstDate),
|
|
||||||
lastDate = utils.dateOnly(lastDate),
|
|
||||||
currentDate = utils.dateOnly(currentDate ?? DateTime.now()),
|
|
||||||
super(key: key) {
|
|
||||||
assert(
|
|
||||||
this.initialStartDate == null || this.initialEndDate == null || !this.initialStartDate!.isAfter(initialEndDate!),
|
|
||||||
'initialStartDate must be on or before initialEndDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
!this.lastDate.isBefore(this.firstDate),
|
|
||||||
'firstDate must be on or before lastDate.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The [DateTime] that represents the start of the initial date range selection.
|
|
||||||
final DateTime? initialStartDate;
|
|
||||||
|
|
||||||
/// The [DateTime] that represents the end of the initial date range selection.
|
|
||||||
final DateTime? initialEndDate;
|
|
||||||
|
|
||||||
/// The earliest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime firstDate;
|
|
||||||
|
|
||||||
/// The latest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime lastDate;
|
|
||||||
|
|
||||||
/// The [DateTime] representing today. It will be highlighted in the day grid.
|
|
||||||
final DateTime currentDate;
|
|
||||||
|
|
||||||
/// Called when the user changes the start date of the selected range.
|
|
||||||
final ValueChanged<DateTime>? onStartDateChanged;
|
|
||||||
|
|
||||||
/// Called when the user changes the end date of the selected range.
|
|
||||||
final ValueChanged<DateTime?>? onEndDateChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CalendarDateRangePickerState createState() => _CalendarDateRangePickerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarDateRangePickerState extends State<CalendarDateRangePicker> {
|
|
||||||
final GlobalKey _scrollViewKey = GlobalKey();
|
|
||||||
DateTime? _startDate;
|
|
||||||
DateTime? _endDate;
|
|
||||||
int _initialMonthIndex = 0;
|
|
||||||
late ScrollController _controller;
|
|
||||||
late bool _showWeekBottomDivider;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = ScrollController();
|
|
||||||
_controller.addListener(_scrollListener);
|
|
||||||
|
|
||||||
_startDate = widget.initialStartDate;
|
|
||||||
_endDate = widget.initialEndDate;
|
|
||||||
|
|
||||||
// Calculate the index for the initially displayed month. This is needed to
|
|
||||||
// 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 = utils.monthDelta(widget.firstDate, initialDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
_showWeekBottomDivider = _initialMonthIndex != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollListener() {
|
|
||||||
if (_controller.offset <= _controller.position.minScrollExtent) {
|
|
||||||
setState(() {
|
|
||||||
_showWeekBottomDivider = false;
|
|
||||||
});
|
|
||||||
} else if (!_showWeekBottomDivider) {
|
|
||||||
setState(() {
|
|
||||||
_showWeekBottomDivider = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int get _numberOfMonths => utils.monthDelta(widget.firstDate, widget.lastDate) + 1;
|
|
||||||
|
|
||||||
void _vibrate() {
|
|
||||||
switch (Theme.of(context).platform) {
|
|
||||||
case TargetPlatform.android:
|
|
||||||
case TargetPlatform.fuchsia:
|
|
||||||
HapticFeedback.vibrate();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This updates the selected date range using this logic:
|
|
||||||
//
|
|
||||||
// * From the unselected state, selecting one date creates the start date.
|
|
||||||
// * If the next selection is before the start date, reset date range and
|
|
||||||
// set the start date to that selection.
|
|
||||||
// * If the next selection is on or after the start date, set the end date
|
|
||||||
// to that selection.
|
|
||||||
// * After both start and end dates are selected, any subsequent selection
|
|
||||||
// resets the date range and sets start date to that selection.
|
|
||||||
void _updateSelection(DateTime date) {
|
|
||||||
_vibrate();
|
|
||||||
setState(() {
|
|
||||||
if (_startDate != null && _endDate == null && !date.isBefore(_startDate!)) {
|
|
||||||
_endDate = date;
|
|
||||||
widget.onEndDateChanged?.call(_endDate);
|
|
||||||
} else {
|
|
||||||
_startDate = date;
|
|
||||||
widget.onStartDateChanged?.call(_startDate!);
|
|
||||||
if (_endDate != null) {
|
|
||||||
_endDate = null;
|
|
||||||
widget.onEndDateChanged?.call(_endDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMonthItem(BuildContext context, int index, bool beforeInitialMonth) {
|
|
||||||
final int monthIndex = beforeInitialMonth
|
|
||||||
? _initialMonthIndex - index - 1
|
|
||||||
: _initialMonthIndex + index;
|
|
||||||
final DateTime month = utils.addMonthsToMonthDate(widget.firstDate, monthIndex);
|
|
||||||
return _MonthItem(
|
|
||||||
selectedDateStart: _startDate,
|
|
||||||
selectedDateEnd: _endDate,
|
|
||||||
currentDate: widget.currentDate,
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
displayedMonth: month,
|
|
||||||
onChanged: _updateSelection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
const Key sliverAfterKey = Key('sliverAfterKey');
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
_DayHeaders(),
|
|
||||||
if (_showWeekBottomDivider) const Divider(height: 0),
|
|
||||||
Expanded(
|
|
||||||
child: _CalendarKeyboardNavigator(
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
initialFocusedDay: _startDate ?? widget.initialStartDate ?? widget.currentDate,
|
|
||||||
// In order to prevent performance issues when displaying the
|
|
||||||
// correct initial month, 2 `SliverList`s are used to split the
|
|
||||||
// months. The first item in the second SliverList is the initial
|
|
||||||
// month to be displayed.
|
|
||||||
child: CustomScrollView(
|
|
||||||
key: _scrollViewKey,
|
|
||||||
controller: _controller,
|
|
||||||
center: sliverAfterKey,
|
|
||||||
slivers: <Widget>[
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) => _buildMonthItem(context, index, true),
|
|
||||||
childCount: _initialMonthIndex,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverList(
|
|
||||||
key: sliverAfterKey,
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) => _buildMonthItem(context, index, false),
|
|
||||||
childCount: _numberOfMonths - _initialMonthIndex,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarKeyboardNavigator extends StatefulWidget {
|
|
||||||
const _CalendarKeyboardNavigator({
|
|
||||||
Key? key,
|
|
||||||
required this.child,
|
|
||||||
required this.firstDate,
|
|
||||||
required this.lastDate,
|
|
||||||
required this.initialFocusedDay,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Widget child;
|
|
||||||
final DateTime firstDate;
|
|
||||||
final DateTime lastDate;
|
|
||||||
final DateTime initialFocusedDay;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_CalendarKeyboardNavigatorState createState() => _CalendarKeyboardNavigatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarKeyboardNavigatorState extends State<_CalendarKeyboardNavigator> {
|
|
||||||
|
|
||||||
late Map<LogicalKeySet, Intent> _shortcutMap;
|
|
||||||
late Map<Type, Action<Intent>> _actionMap;
|
|
||||||
late FocusNode _dayGridFocus;
|
|
||||||
TraversalDirection? _dayTraversalDirection;
|
|
||||||
DateTime? _focusedDay;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
_shortcutMap = <LogicalKeySet, Intent>{
|
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
|
|
||||||
};
|
|
||||||
_actionMap = <Type, Action<Intent>>{
|
|
||||||
NextFocusIntent: CallbackAction<NextFocusIntent>(onInvoke: _handleGridNextFocus),
|
|
||||||
PreviousFocusIntent: CallbackAction<PreviousFocusIntent>(onInvoke: _handleGridPreviousFocus),
|
|
||||||
DirectionalFocusIntent: CallbackAction<DirectionalFocusIntent>(onInvoke: _handleDirectionFocus),
|
|
||||||
};
|
|
||||||
_dayGridFocus = FocusNode(debugLabel: 'Day Grid');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_dayGridFocus.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleGridFocusChange(bool focused) {
|
|
||||||
setState(() {
|
|
||||||
if (focused) {
|
|
||||||
_focusedDay ??= widget.initialFocusedDay;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move focus to the next element after the day grid.
|
|
||||||
void _handleGridNextFocus(NextFocusIntent intent) {
|
|
||||||
_dayGridFocus.requestFocus();
|
|
||||||
_dayGridFocus.nextFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move focus to the previous element before the day grid.
|
|
||||||
void _handleGridPreviousFocus(PreviousFocusIntent intent) {
|
|
||||||
_dayGridFocus.requestFocus();
|
|
||||||
_dayGridFocus.previousFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move the internal focus date in the direction of the given intent.
|
|
||||||
///
|
|
||||||
/// This will attempt to move the focused day to the next selectable day in
|
|
||||||
/// the given direction. If the new date is not in the current month, then
|
|
||||||
/// the page view will be scrolled to show the new date's month.
|
|
||||||
///
|
|
||||||
/// For horizontal directions, it will move forward or backward a day (depending
|
|
||||||
/// on the current [TextDirection]). For vertical directions it will move up and
|
|
||||||
/// down a week at a time.
|
|
||||||
void _handleDirectionFocus(DirectionalFocusIntent intent) {
|
|
||||||
assert(_focusedDay != null);
|
|
||||||
setState(() {
|
|
||||||
final DateTime? nextDate = _nextDateInDirection(_focusedDay!, intent.direction);
|
|
||||||
if (nextDate != null) {
|
|
||||||
_focusedDay = nextDate;
|
|
||||||
_dayTraversalDirection = intent.direction;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static const Map<TraversalDirection, int> _directionOffset = <TraversalDirection, int>{
|
|
||||||
TraversalDirection.up: -DateTime.daysPerWeek,
|
|
||||||
TraversalDirection.right: 1,
|
|
||||||
TraversalDirection.down: DateTime.daysPerWeek,
|
|
||||||
TraversalDirection.left: -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
int _dayDirectionOffset(TraversalDirection traversalDirection, TextDirection textDirection) {
|
|
||||||
// Swap left and right if the text direction if RTL
|
|
||||||
if (textDirection == TextDirection.rtl) {
|
|
||||||
if (traversalDirection == TraversalDirection.left)
|
|
||||||
traversalDirection = TraversalDirection.right;
|
|
||||||
else if (traversalDirection == TraversalDirection.right)
|
|
||||||
traversalDirection = TraversalDirection.left;
|
|
||||||
}
|
|
||||||
return _directionOffset[traversalDirection]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? _nextDateInDirection(DateTime date, TraversalDirection direction) {
|
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
|
||||||
final DateTime nextDate = utils.addDaysToDate(date, _dayDirectionOffset(direction, textDirection));
|
|
||||||
if (!nextDate.isBefore(widget.firstDate) && !nextDate.isAfter(widget.lastDate)) {
|
|
||||||
return nextDate;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FocusableActionDetector(
|
|
||||||
shortcuts: _shortcutMap,
|
|
||||||
actions: _actionMap,
|
|
||||||
focusNode: _dayGridFocus,
|
|
||||||
onFocusChange: _handleGridFocusChange,
|
|
||||||
child: _FocusedDate(
|
|
||||||
date: _dayGridFocus.hasFocus ? _focusedDay : null,
|
|
||||||
scrollDirection: _dayGridFocus.hasFocus ? _dayTraversalDirection : null,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// InheritedWidget indicating what the current focused date is for its children.
|
|
||||||
///
|
|
||||||
/// This is used by the [_MonthPicker] to let its children [_DayPicker]s know
|
|
||||||
/// what the currently focused date (if any) should be.
|
|
||||||
class _FocusedDate extends InheritedWidget {
|
|
||||||
const _FocusedDate({
|
|
||||||
Key? key,
|
|
||||||
required Widget child,
|
|
||||||
this.date,
|
|
||||||
this.scrollDirection,
|
|
||||||
}) : super(key: key, child: child);
|
|
||||||
|
|
||||||
final DateTime? date;
|
|
||||||
final TraversalDirection? scrollDirection;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool updateShouldNotify(_FocusedDate oldWidget) {
|
|
||||||
return !utils.isSameDay(date, oldWidget.date) || scrollDirection != oldWidget.scrollDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
static _FocusedDate? of(BuildContext context) {
|
|
||||||
return context.dependOnInheritedWidgetOfExactType<_FocusedDate>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _DayHeaders extends StatelessWidget {
|
|
||||||
/// Builds widgets showing abbreviated days of week. The first widget in the
|
|
||||||
/// returned list corresponds to the first day of week for the current locale.
|
|
||||||
///
|
|
||||||
/// Examples:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// ┌ Sunday is the first day of week in the US (en_US)
|
|
||||||
/// |
|
|
||||||
/// S M T W T F S <-- the returned list contains these widgets
|
|
||||||
/// _ _ _ _ _ 1 2
|
|
||||||
/// 3 4 5 6 7 8 9
|
|
||||||
///
|
|
||||||
/// ┌ But it's Monday in the UK (en_GB)
|
|
||||||
/// |
|
|
||||||
/// M T W T F S S <-- the returned list contains these widgets
|
|
||||||
/// _ _ _ _ 1 2 3
|
|
||||||
/// 4 5 6 7 8 9 10
|
|
||||||
/// ```
|
|
||||||
List<Widget> _getDayHeaders(TextStyle headerStyle, MaterialLocalizations localizations) {
|
|
||||||
final List<Widget> result = <Widget>[];
|
|
||||||
for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) {
|
|
||||||
final String weekday = localizations.narrowWeekdays[i];
|
|
||||||
result.add(ExcludeSemantics(
|
|
||||||
child: Center(child: Text(weekday, style: headerStyle)),
|
|
||||||
));
|
|
||||||
if (i == (localizations.firstDayOfWeekIndex - 1) % 7)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = themeData.colorScheme;
|
|
||||||
final TextStyle textStyle = themeData.textTheme.subtitle2!.apply(color: colorScheme.onSurface);
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final List<Widget> labels = _getDayHeaders(textStyle, localizations);
|
|
||||||
|
|
||||||
// Add leading and trailing containers for edges of the custom grid layout.
|
|
||||||
labels.insert(0, Container());
|
|
||||||
labels.add(Container());
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: MediaQuery.of(context).orientation == Orientation.landscape
|
|
||||||
? _maxCalendarWidthLandscape
|
|
||||||
: _maxCalendarWidthPortrait,
|
|
||||||
maxHeight: _monthItemRowHeight,
|
|
||||||
),
|
|
||||||
child: GridView.custom(
|
|
||||||
shrinkWrap: true,
|
|
||||||
gridDelegate: _monthItemGridDelegate,
|
|
||||||
childrenDelegate: SliverChildListDelegate(
|
|
||||||
labels,
|
|
||||||
addRepaintBoundaries: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MonthItemGridDelegate extends SliverGridDelegate {
|
|
||||||
const _MonthItemGridDelegate();
|
|
||||||
|
|
||||||
@override
|
|
||||||
SliverGridLayout getLayout(SliverConstraints constraints) {
|
|
||||||
final double tileWidth = (constraints.crossAxisExtent - 2 * _horizontalPadding) / DateTime.daysPerWeek;
|
|
||||||
return _MonthSliverGridLayout(
|
|
||||||
crossAxisCount: DateTime.daysPerWeek + 2,
|
|
||||||
dayChildWidth: tileWidth,
|
|
||||||
edgeChildWidth: _horizontalPadding,
|
|
||||||
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRelayout(_MonthItemGridDelegate oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _MonthItemGridDelegate _monthItemGridDelegate = _MonthItemGridDelegate();
|
|
||||||
|
|
||||||
class _MonthSliverGridLayout extends SliverGridLayout {
|
|
||||||
/// Creates a layout that uses equally sized and spaced tiles for each day of
|
|
||||||
/// the week and an additional edge tile for padding at the start and end of
|
|
||||||
/// each row.
|
|
||||||
///
|
|
||||||
/// This is necessary to facilitate the painting of the range highlight
|
|
||||||
/// correctly.
|
|
||||||
const _MonthSliverGridLayout({
|
|
||||||
required this.crossAxisCount,
|
|
||||||
required this.dayChildWidth,
|
|
||||||
required this.edgeChildWidth,
|
|
||||||
required this.reverseCrossAxis,
|
|
||||||
}) : assert(crossAxisCount != null && crossAxisCount > 0),
|
|
||||||
assert(dayChildWidth != null && dayChildWidth >= 0),
|
|
||||||
assert(edgeChildWidth != null && edgeChildWidth >= 0),
|
|
||||||
assert(reverseCrossAxis != null);
|
|
||||||
|
|
||||||
/// The number of children in the cross axis.
|
|
||||||
final int crossAxisCount;
|
|
||||||
|
|
||||||
/// The width in logical pixels of the day child widgets.
|
|
||||||
final double dayChildWidth;
|
|
||||||
|
|
||||||
/// The width in logical pixels of the edge child widgets.
|
|
||||||
final double edgeChildWidth;
|
|
||||||
|
|
||||||
/// Whether the children should be placed in the opposite order of increasing
|
|
||||||
/// coordinates in the cross axis.
|
|
||||||
///
|
|
||||||
/// For example, if the cross axis is horizontal, the children are placed from
|
|
||||||
/// left to right when [reverseCrossAxis] is false and from right to left when
|
|
||||||
/// [reverseCrossAxis] is true.
|
|
||||||
///
|
|
||||||
/// Typically set to the return value of [axisDirectionIsReversed] applied to
|
|
||||||
/// the [SliverConstraints.crossAxisDirection].
|
|
||||||
final bool reverseCrossAxis;
|
|
||||||
|
|
||||||
/// The number of logical pixels from the leading edge of one row to the
|
|
||||||
/// leading edge of the next row.
|
|
||||||
double get _rowHeight {
|
|
||||||
return _monthItemRowHeight + _monthItemSpaceBetweenRows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The height in logical pixels of the children widgets.
|
|
||||||
double get _childHeight {
|
|
||||||
return _monthItemRowHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
|
||||||
return crossAxisCount * (scrollOffset ~/ _rowHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
|
||||||
final int mainAxisCount = (scrollOffset / _rowHeight).ceil();
|
|
||||||
return math.max(0, crossAxisCount * mainAxisCount - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
double _getCrossAxisOffset(double crossAxisStart, bool isPadding) {
|
|
||||||
if (reverseCrossAxis) {
|
|
||||||
return
|
|
||||||
((crossAxisCount - 2) * dayChildWidth + 2 * edgeChildWidth) -
|
|
||||||
crossAxisStart -
|
|
||||||
(isPadding ? edgeChildWidth : dayChildWidth);
|
|
||||||
}
|
|
||||||
return crossAxisStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SliverGridGeometry getGeometryForChildIndex(int index) {
|
|
||||||
final int adjustedIndex = index % crossAxisCount;
|
|
||||||
final bool isEdge = adjustedIndex == 0 || adjustedIndex == crossAxisCount - 1;
|
|
||||||
final double crossAxisStart = math.max(0, (adjustedIndex - 1) * dayChildWidth + edgeChildWidth);
|
|
||||||
|
|
||||||
return SliverGridGeometry(
|
|
||||||
scrollOffset: (index ~/ crossAxisCount) * _rowHeight,
|
|
||||||
crossAxisOffset: _getCrossAxisOffset(crossAxisStart, isEdge),
|
|
||||||
mainAxisExtent: _childHeight,
|
|
||||||
crossAxisExtent: isEdge ? edgeChildWidth : dayChildWidth,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeMaxScrollOffset(int childCount) {
|
|
||||||
assert(childCount >= 0);
|
|
||||||
final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
|
|
||||||
final double mainAxisSpacing = _rowHeight - _childHeight;
|
|
||||||
return _rowHeight * mainAxisCount - mainAxisSpacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays the days of a given month and allows choosing a date range.
|
|
||||||
///
|
|
||||||
/// The days are arranged in a rectangular grid with one column for each day of
|
|
||||||
/// the week.
|
|
||||||
class _MonthItem extends StatefulWidget {
|
|
||||||
/// Creates a month item.
|
|
||||||
_MonthItem({
|
|
||||||
Key? key,
|
|
||||||
required this.selectedDateStart,
|
|
||||||
required this.selectedDateEnd,
|
|
||||||
required this.currentDate,
|
|
||||||
required this.onChanged,
|
|
||||||
required this.firstDate,
|
|
||||||
required this.lastDate,
|
|
||||||
required this.displayedMonth,
|
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
|
||||||
}) : assert(firstDate != null),
|
|
||||||
assert(lastDate != null),
|
|
||||||
assert(!firstDate.isAfter(lastDate)),
|
|
||||||
assert(selectedDateStart == null || !selectedDateStart.isBefore(firstDate)),
|
|
||||||
assert(selectedDateEnd == null || !selectedDateEnd.isBefore(firstDate)),
|
|
||||||
assert(selectedDateStart == null || !selectedDateStart.isAfter(lastDate)),
|
|
||||||
assert(selectedDateEnd == null || !selectedDateEnd.isAfter(lastDate)),
|
|
||||||
assert(selectedDateStart == null || selectedDateEnd == null || !selectedDateStart.isAfter(selectedDateEnd)),
|
|
||||||
assert(currentDate != null),
|
|
||||||
assert(onChanged != null),
|
|
||||||
assert(displayedMonth != null),
|
|
||||||
assert(dragStartBehavior != null),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// The currently selected start date.
|
|
||||||
///
|
|
||||||
/// This date is highlighted in the picker.
|
|
||||||
final DateTime? selectedDateStart;
|
|
||||||
|
|
||||||
/// The currently selected end date.
|
|
||||||
///
|
|
||||||
/// This date is highlighted in the picker.
|
|
||||||
final DateTime? selectedDateEnd;
|
|
||||||
|
|
||||||
/// The current date at the time the picker is displayed.
|
|
||||||
final DateTime currentDate;
|
|
||||||
|
|
||||||
/// Called when the user picks a day.
|
|
||||||
final ValueChanged<DateTime> onChanged;
|
|
||||||
|
|
||||||
/// The earliest date the user is permitted to pick.
|
|
||||||
final DateTime firstDate;
|
|
||||||
|
|
||||||
/// The latest date the user is permitted to pick.
|
|
||||||
final DateTime lastDate;
|
|
||||||
|
|
||||||
/// The month whose days are displayed by this picker.
|
|
||||||
final DateTime displayedMonth;
|
|
||||||
|
|
||||||
/// Determines the way that drag start behavior is handled.
|
|
||||||
///
|
|
||||||
/// If set to [DragStartBehavior.start], the drag gesture used to scroll a
|
|
||||||
/// date picker wheel will begin upon the detection of a drag gesture. If set
|
|
||||||
/// to [DragStartBehavior.down] it will begin when a down event is first
|
|
||||||
/// detected.
|
|
||||||
///
|
|
||||||
/// In general, setting this to [DragStartBehavior.start] will make drag
|
|
||||||
/// animation smoother and setting it to [DragStartBehavior.down] will make
|
|
||||||
/// drag behavior feel slightly more reactive.
|
|
||||||
///
|
|
||||||
/// By default, the drag start behavior is [DragStartBehavior.start].
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
|
|
||||||
/// the different behaviors.
|
|
||||||
final DragStartBehavior dragStartBehavior;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MonthItemState createState() => _MonthItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MonthItemState extends State<_MonthItem> {
|
|
||||||
/// List of [FocusNode]s, one for each day of the month.
|
|
||||||
late List<FocusNode> _dayFocusNodes;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final int daysInMonth = utils.getDaysInMonth(widget.displayedMonth.year, widget.displayedMonth.month);
|
|
||||||
_dayFocusNodes = List<FocusNode>.generate(
|
|
||||||
daysInMonth,
|
|
||||||
(int index) => FocusNode(skipTraversal: true, debugLabel: 'Day ${index + 1}')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
// Check to see if the focused date is in this month, if so focus it.
|
|
||||||
final DateTime? focusedDate = _FocusedDate.of(context)?.date;
|
|
||||||
if (focusedDate != null && utils.isSameMonth(widget.displayedMonth, focusedDate)) {
|
|
||||||
_dayFocusNodes[focusedDate.day - 1].requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
for (final FocusNode node in _dayFocusNodes) {
|
|
||||||
node.dispose();
|
|
||||||
}
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _highlightColor(BuildContext context) {
|
|
||||||
return Theme.of(context).colorScheme.primary.withOpacity(0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _dayFocusChanged(bool focused) {
|
|
||||||
if (focused) {
|
|
||||||
final TraversalDirection? focusDirection = _FocusedDate.of(context)?.scrollDirection;
|
|
||||||
if (focusDirection != null) {
|
|
||||||
ScrollPositionAlignmentPolicy policy = ScrollPositionAlignmentPolicy.explicit;
|
|
||||||
switch (focusDirection) {
|
|
||||||
case TraversalDirection.up:
|
|
||||||
case TraversalDirection.left:
|
|
||||||
policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
|
|
||||||
break;
|
|
||||||
case TraversalDirection.right:
|
|
||||||
case TraversalDirection.down:
|
|
||||||
policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Scrollable.ensureVisible(primaryFocus!.context!,
|
|
||||||
duration: _monthScrollDuration,
|
|
||||||
alignmentPolicy: policy,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDayItem(BuildContext context, DateTime dayToBuild, int firstDayOffset, int daysInMonth) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
|
||||||
final TextTheme textTheme = theme.textTheme;
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final TextDirection textDirection = Directionality.of(context);
|
|
||||||
final Color highlightColor = _highlightColor(context);
|
|
||||||
final int day = dayToBuild.day;
|
|
||||||
|
|
||||||
final bool isDisabled = dayToBuild.isAfter(widget.lastDate) || dayToBuild.isBefore(widget.firstDate);
|
|
||||||
|
|
||||||
BoxDecoration? decoration;
|
|
||||||
TextStyle? itemStyle = textTheme.bodyText2;
|
|
||||||
|
|
||||||
final bool isRangeSelected = widget.selectedDateStart != null && widget.selectedDateEnd != null;
|
|
||||||
final bool isSelectedDayStart = widget.selectedDateStart != null && dayToBuild.isAtSameMomentAs(widget.selectedDateStart!);
|
|
||||||
final bool isSelectedDayEnd = widget.selectedDateEnd != null && dayToBuild.isAtSameMomentAs(widget.selectedDateEnd!);
|
|
||||||
final bool isInRange = isRangeSelected &&
|
|
||||||
dayToBuild.isAfter(widget.selectedDateStart!) &&
|
|
||||||
dayToBuild.isBefore(widget.selectedDateEnd!);
|
|
||||||
|
|
||||||
_HighlightPainter? highlightPainter;
|
|
||||||
|
|
||||||
if (isSelectedDayStart || isSelectedDayEnd) {
|
|
||||||
// The selected start and end dates gets a circle background
|
|
||||||
// highlight, and a contrasting text color.
|
|
||||||
itemStyle = textTheme.bodyText2?.apply(color: colorScheme.onPrimary);
|
|
||||||
decoration = BoxDecoration(
|
|
||||||
color: colorScheme.primary,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isRangeSelected && widget.selectedDateStart != widget.selectedDateEnd) {
|
|
||||||
final _HighlightPainterStyle style = isSelectedDayStart
|
|
||||||
? _HighlightPainterStyle.highlightTrailing
|
|
||||||
: _HighlightPainterStyle.highlightLeading;
|
|
||||||
highlightPainter = _HighlightPainter(
|
|
||||||
color: highlightColor,
|
|
||||||
style: style,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (isInRange) {
|
|
||||||
// The days within the range get a light background highlight.
|
|
||||||
highlightPainter = _HighlightPainter(
|
|
||||||
color: highlightColor,
|
|
||||||
style: _HighlightPainterStyle.highlightAll,
|
|
||||||
textDirection: textDirection,
|
|
||||||
);
|
|
||||||
} else if (isDisabled) {
|
|
||||||
itemStyle = textTheme.bodyText2?.apply(color: colorScheme.onSurface.withOpacity(0.38));
|
|
||||||
} else if (utils.isSameDay(widget.currentDate, dayToBuild)) {
|
|
||||||
// The current day gets a different text color and a circle stroke
|
|
||||||
// border.
|
|
||||||
itemStyle = textTheme.bodyText2?.apply(color: colorScheme.primary);
|
|
||||||
decoration = BoxDecoration(
|
|
||||||
border: Border.all(color: colorScheme.primary, width: 1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want the day of month to be spoken first irrespective of the
|
|
||||||
// locale-specific preferences or TextDirection. This is because
|
|
||||||
// an accessibility user is more likely to be interested in the
|
|
||||||
// day of month before the rest of the date, as they are looking
|
|
||||||
// for the day of month. To do that we prepend day of month to the
|
|
||||||
// formatted full date.
|
|
||||||
String semanticLabel = '${localizations.formatDecimal(day)}, ${localizations.formatFullDate(dayToBuild)}';
|
|
||||||
if (isSelectedDayStart) {
|
|
||||||
semanticLabel = localizations.dateRangeStartDateSemanticLabel(semanticLabel);
|
|
||||||
} else if (isSelectedDayEnd) {
|
|
||||||
semanticLabel = localizations.dateRangeEndDateSemanticLabel(semanticLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget dayWidget = Container(
|
|
||||||
decoration: decoration,
|
|
||||||
child: Center(
|
|
||||||
child: Semantics(
|
|
||||||
label: semanticLabel,
|
|
||||||
selected: isSelectedDayStart || isSelectedDayEnd,
|
|
||||||
child: ExcludeSemantics(
|
|
||||||
child: Text(localizations.formatDecimal(day), style: itemStyle),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (highlightPainter != null) {
|
|
||||||
dayWidget = CustomPaint(
|
|
||||||
painter: highlightPainter,
|
|
||||||
child: dayWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDisabled) {
|
|
||||||
dayWidget = InkResponse(
|
|
||||||
focusNode: _dayFocusNodes[day - 1],
|
|
||||||
onTap: () => widget.onChanged(dayToBuild),
|
|
||||||
radius: _monthItemRowHeight / 2 + 4,
|
|
||||||
splashColor: colorScheme.primary.withOpacity(0.38),
|
|
||||||
onFocusChange: _dayFocusChanged,
|
|
||||||
child: dayWidget,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dayWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEdgeContainer(BuildContext context, bool isHighlighted) {
|
|
||||||
return Container(color: isHighlighted ? _highlightColor(context) : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final TextTheme textTheme = themeData.textTheme;
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final int year = widget.displayedMonth.year;
|
|
||||||
final int month = widget.displayedMonth.month;
|
|
||||||
final int daysInMonth = utils.getDaysInMonth(year, month);
|
|
||||||
final int dayOffset = utils.firstDayOffset(year, month, localizations);
|
|
||||||
final int weeks = ((daysInMonth + dayOffset) / DateTime.daysPerWeek).ceil();
|
|
||||||
final double gridHeight =
|
|
||||||
weeks * _monthItemRowHeight + (weeks - 1) * _monthItemSpaceBetweenRows;
|
|
||||||
final List<Widget> dayItems = <Widget>[];
|
|
||||||
|
|
||||||
for (int i = 0; true; i += 1) {
|
|
||||||
// 1-based day of month, e.g. 1-31 for January, and 1-29 for February on
|
|
||||||
// a leap year.
|
|
||||||
final int day = i - dayOffset + 1;
|
|
||||||
if (day > daysInMonth)
|
|
||||||
break;
|
|
||||||
if (day < 1) {
|
|
||||||
dayItems.add(Container());
|
|
||||||
} else {
|
|
||||||
final DateTime dayToBuild = DateTime(year, month, day);
|
|
||||||
final Widget dayItem = _buildDayItem(
|
|
||||||
context,
|
|
||||||
dayToBuild,
|
|
||||||
dayOffset,
|
|
||||||
daysInMonth,
|
|
||||||
);
|
|
||||||
dayItems.add(dayItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the leading/trailing edge containers to each week in order to
|
|
||||||
// correctly extend the range highlight.
|
|
||||||
final List<Widget> paddedDayItems = <Widget>[];
|
|
||||||
for (int i = 0; i < weeks; i++) {
|
|
||||||
final int start = i * DateTime.daysPerWeek;
|
|
||||||
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);
|
|
||||||
// Only color the edge container if it is after the start date and
|
|
||||||
// on/before the end date.
|
|
||||||
final bool isLeadingInRange =
|
|
||||||
!(dayOffset > 0 && i == 0) &&
|
|
||||||
widget.selectedDateStart != null &&
|
|
||||||
widget.selectedDateEnd != null &&
|
|
||||||
dateAfterLeadingPadding.isAfter(widget.selectedDateStart!) &&
|
|
||||||
!dateAfterLeadingPadding.isAfter(widget.selectedDateEnd!);
|
|
||||||
weekList.insert(0, _buildEdgeContainer(context, isLeadingInRange));
|
|
||||||
|
|
||||||
// Only add a trailing edge container if it is for a full week and not a
|
|
||||||
// partial week.
|
|
||||||
if (end < dayItems.length || (end == dayItems.length && dayItems.length % DateTime.daysPerWeek == 0)) {
|
|
||||||
final DateTime dateBeforeTrailingPadding =
|
|
||||||
DateTime(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 =
|
|
||||||
widget.selectedDateStart != null &&
|
|
||||||
widget.selectedDateEnd != null &&
|
|
||||||
!dateBeforeTrailingPadding.isBefore(widget.selectedDateStart!) &&
|
|
||||||
dateBeforeTrailingPadding.isBefore(widget.selectedDateEnd!);
|
|
||||||
weekList.add(_buildEdgeContainer(context, isTrailingInRange));
|
|
||||||
}
|
|
||||||
|
|
||||||
paddedDayItems.addAll(weekList);
|
|
||||||
}
|
|
||||||
|
|
||||||
final double maxWidth = MediaQuery.of(context).orientation == Orientation.landscape
|
|
||||||
? _maxCalendarWidthLandscape
|
|
||||||
: _maxCalendarWidthPortrait;
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
||||||
height: _monthItemHeaderHeight,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: ExcludeSemantics(
|
|
||||||
child: Text(
|
|
||||||
localizations.formatMonthYear(widget.displayedMonth),
|
|
||||||
style: textTheme.bodyText2!.apply(color: themeData.colorScheme.onSurface),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: maxWidth,
|
|
||||||
maxHeight: gridHeight,
|
|
||||||
),
|
|
||||||
child: GridView.custom(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
gridDelegate: _monthItemGridDelegate,
|
|
||||||
childrenDelegate: SliverChildListDelegate(
|
|
||||||
paddedDayItems,
|
|
||||||
addRepaintBoundaries: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: _monthItemFooterHeight),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines which style to use to paint the highlight.
|
|
||||||
enum _HighlightPainterStyle {
|
|
||||||
/// Paints nothing.
|
|
||||||
none,
|
|
||||||
|
|
||||||
/// Paints a rectangle that occupies the leading half of the space.
|
|
||||||
highlightLeading,
|
|
||||||
|
|
||||||
/// Paints a rectangle that occupies the trailing half of the space.
|
|
||||||
highlightTrailing,
|
|
||||||
|
|
||||||
/// Paints a rectangle that occupies all available space.
|
|
||||||
highlightAll,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This custom painter will add a background highlight to its child.
|
|
||||||
///
|
|
||||||
/// This highlight will be drawn depending on the [style], [color], and
|
|
||||||
/// [textDirection] supplied. It will either paint a rectangle on the
|
|
||||||
/// left/right, a full rectangle, or nothing at all. This logic is determined by
|
|
||||||
/// a combination of the [style] and [textDirection].
|
|
||||||
class _HighlightPainter extends CustomPainter {
|
|
||||||
_HighlightPainter({
|
|
||||||
required this.color,
|
|
||||||
this.style = _HighlightPainterStyle.none,
|
|
||||||
this.textDirection,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Color color;
|
|
||||||
final _HighlightPainterStyle style;
|
|
||||||
final TextDirection? textDirection;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
if (style == _HighlightPainterStyle.none) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Paint paint = Paint()
|
|
||||||
..color = color
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final Rect rectLeft = Rect.fromLTWH(0, 0, size.width / 2, size.height);
|
|
||||||
final Rect rectRight = Rect.fromLTWH(size.width / 2, 0, size.width / 2, size.height);
|
|
||||||
|
|
||||||
switch (style) {
|
|
||||||
case _HighlightPainterStyle.highlightTrailing:
|
|
||||||
canvas.drawRect(
|
|
||||||
textDirection == TextDirection.ltr ? rectRight : rectLeft,
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case _HighlightPainterStyle.highlightLeading:
|
|
||||||
canvas.drawRect(
|
|
||||||
textDirection == TextDirection.ltr ? rectLeft : rectRight,
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case _HighlightPainterStyle.highlightAll:
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case _HighlightPainterStyle.none:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'dart:ui' show hashValues;
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
/// Mode of the date picker dialog.
|
|
||||||
///
|
|
||||||
/// Either a calendar or text input. In [calendar] mode, a calendar view is
|
|
||||||
/// displayed and the user taps the day they wish to select. In [input] mode a
|
|
||||||
/// [TextField] is displayed and the user types in the date they wish to select.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [showDatePicker] and [showDateRangePicker], which use this to control
|
|
||||||
/// the initial entry mode of their dialogs.
|
|
||||||
enum DatePickerEntryMode {
|
|
||||||
/// Tapping on a calendar.
|
|
||||||
calendar,
|
|
||||||
|
|
||||||
/// Text input.
|
|
||||||
input,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initial display of a calendar date picker.
|
|
||||||
///
|
|
||||||
/// Either a grid of available years or a monthly calendar.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [showDatePicker], which shows a dialog that contains a material design
|
|
||||||
/// date picker.
|
|
||||||
/// * [CalendarDatePicker], widget which implements the material design date picker.
|
|
||||||
enum DatePickerMode {
|
|
||||||
/// Choosing a month and day.
|
|
||||||
day,
|
|
||||||
|
|
||||||
/// Choosing a year.
|
|
||||||
year,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signature for predicating dates for enabled date selections.
|
|
||||||
///
|
|
||||||
/// See [showDatePicker], which has a [SelectableDayPredicate] parameter used
|
|
||||||
/// to specify allowable days in the date picker.
|
|
||||||
typedef SelectableDayPredicate = bool Function(DateTime day);
|
|
||||||
|
|
||||||
/// Encapsulates a start and end [DateTime] that represent the range of dates
|
|
||||||
/// between them.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
/// * [showDateRangePicker], which displays a dialog that allows the user to
|
|
||||||
/// select a date range.
|
|
||||||
@immutable
|
|
||||||
class DateTimeRange {
|
|
||||||
/// Creates a date range for the given start and end [DateTime].
|
|
||||||
///
|
|
||||||
/// [start] and [end] must be non-null.
|
|
||||||
const DateTimeRange({
|
|
||||||
required this.start,
|
|
||||||
required this.end,
|
|
||||||
}) : assert(start != null),
|
|
||||||
assert(end != null);
|
|
||||||
|
|
||||||
/// The start of the range of dates.
|
|
||||||
final DateTime start;
|
|
||||||
|
|
||||||
/// The end of the range of dates.
|
|
||||||
final DateTime end;
|
|
||||||
|
|
||||||
/// Returns a [Duration] of the time between [start] and [end].
|
|
||||||
///
|
|
||||||
/// See [DateTime.difference] for more details.
|
|
||||||
Duration get duration => end.difference(start);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (other.runtimeType != runtimeType)
|
|
||||||
return false;
|
|
||||||
return other is DateTimeRange
|
|
||||||
&& other.start == start
|
|
||||||
&& other.end == end;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hashValues(start, end);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '$start - $end';
|
|
||||||
}
|
|
|
@ -1,511 +0,0 @@
|
||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import '../color_scheme.dart';
|
|
||||||
import '../debug.dart';
|
|
||||||
import '../dialog.dart';
|
|
||||||
import '../icons.dart';
|
|
||||||
import '../material_localizations.dart';
|
|
||||||
import '../text_button.dart';
|
|
||||||
import '../text_theme.dart';
|
|
||||||
import '../theme.dart';
|
|
||||||
|
|
||||||
import 'calendar_date_picker.dart';
|
|
||||||
import 'date_picker_common.dart';
|
|
||||||
import 'date_picker_header.dart';
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
import 'input_date_picker.dart';
|
|
||||||
|
|
||||||
const Size _calendarPortraitDialogSize = Size(330.0, 518.0);
|
|
||||||
const Size _calendarLandscapeDialogSize = Size(496.0, 346.0);
|
|
||||||
const Size _inputPortraitDialogSize = Size(330.0, 270.0);
|
|
||||||
const Size _inputLandscapeDialogSize = Size(496, 160.0);
|
|
||||||
const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
|
|
||||||
const double _inputFormPortraitHeight = 98.0;
|
|
||||||
const double _inputFormLandscapeHeight = 108.0;
|
|
||||||
|
|
||||||
|
|
||||||
/// Shows a dialog containing a Material Design date picker.
|
|
||||||
///
|
|
||||||
/// The returned [Future] resolves to the date selected by the user when the
|
|
||||||
/// user confirms the dialog. If the user cancels the dialog, null is returned.
|
|
||||||
///
|
|
||||||
/// When the date picker is first displayed, it will show the month of
|
|
||||||
/// [initialDate], with [initialDate] selected.
|
|
||||||
///
|
|
||||||
/// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
|
|
||||||
/// allowable date. [initialDate] must either fall between these dates,
|
|
||||||
/// or be equal to one of them. For each of these [DateTime] parameters, only
|
|
||||||
/// their dates are considered. Their time fields are ignored. They must all
|
|
||||||
/// be non-null.
|
|
||||||
///
|
|
||||||
/// The [currentDate] represents the current day (i.e. today). This
|
|
||||||
/// date will be highlighted in the day grid. If null, the date of
|
|
||||||
/// `DateTime.now()` will be used.
|
|
||||||
///
|
|
||||||
/// An optional [initialEntryMode] argument can be used to display the date
|
|
||||||
/// picker in the [DatePickerEntryMode.calendar] (a calendar month grid)
|
|
||||||
/// or [DatePickerEntryMode.input] (a text input field) mode.
|
|
||||||
/// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
|
|
||||||
///
|
|
||||||
/// An optional [selectableDayPredicate] function can be passed in to only allow
|
|
||||||
/// certain days for selection. If provided, only the days that
|
|
||||||
/// [selectableDayPredicate] returns true for will be selectable. For example,
|
|
||||||
/// this can be used to only allow weekdays for selection. If provided, it must
|
|
||||||
/// return true for [initialDate].
|
|
||||||
///
|
|
||||||
/// The following optional string parameters allow you to override the default
|
|
||||||
/// text used for various parts of the dialog:
|
|
||||||
///
|
|
||||||
/// * [helpText], label displayed at the top of the dialog.
|
|
||||||
/// * [cancelText], label on the cancel button.
|
|
||||||
/// * [confirmText], label on the ok button.
|
|
||||||
/// * [errorFormatText], message used when the input text isn't in a proper date format.
|
|
||||||
/// * [errorInvalidText], message used when the input text isn't a selectable date.
|
|
||||||
/// * [fieldHintText], text used to prompt the user when no text has been entered in the field.
|
|
||||||
/// * [fieldLabelText], label for the date text input field.
|
|
||||||
///
|
|
||||||
/// An optional [locale] argument can be used to set the locale for the date
|
|
||||||
/// picker. It defaults to the ambient locale provided by [Localizations].
|
|
||||||
///
|
|
||||||
/// An optional [textDirection] argument can be used to set the text direction
|
|
||||||
/// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
|
|
||||||
/// defaults to the ambient text direction provided by [Directionality]. If both
|
|
||||||
/// [locale] and [textDirection] are non-null, [textDirection] overrides the
|
|
||||||
/// direction chosen for the [locale].
|
|
||||||
///
|
|
||||||
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed to
|
|
||||||
/// [showDialog], the documentation for which discusses how it is used. [context]
|
|
||||||
/// and [useRootNavigator] must be non-null.
|
|
||||||
///
|
|
||||||
/// The [builder] parameter can be used to wrap the dialog widget
|
|
||||||
/// to add inherited widgets like [Theme].
|
|
||||||
///
|
|
||||||
/// An optional [initialDatePickerMode] argument can be used to have the
|
|
||||||
/// calendar date picker initially appear in the [DatePickerMode.year] or
|
|
||||||
/// [DatePickerMode.day] mode. It defaults to [DatePickerMode.day], and
|
|
||||||
/// must be non-null.
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [showDateRangePicker], which shows a material design date range picker
|
|
||||||
/// used to select a range of dates.
|
|
||||||
/// * [CalendarDatePicker], which provides the calendar grid used by the date picker dialog.
|
|
||||||
/// * [InputDatePickerFormField], which provides a text input field for entering dates.
|
|
||||||
///
|
|
||||||
Future<DateTime?> showDatePicker({
|
|
||||||
required BuildContext context,
|
|
||||||
required DateTime initialDate,
|
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
DateTime? currentDate,
|
|
||||||
DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
|
|
||||||
SelectableDayPredicate? selectableDayPredicate,
|
|
||||||
String? helpText,
|
|
||||||
String? cancelText,
|
|
||||||
String? confirmText,
|
|
||||||
Locale? locale,
|
|
||||||
bool useRootNavigator = true,
|
|
||||||
RouteSettings? routeSettings,
|
|
||||||
TextDirection? textDirection,
|
|
||||||
TransitionBuilder? builder,
|
|
||||||
DatePickerMode initialDatePickerMode = DatePickerMode.day,
|
|
||||||
String? errorFormatText,
|
|
||||||
String? errorInvalidText,
|
|
||||||
String? fieldHintText,
|
|
||||||
String? fieldLabelText,
|
|
||||||
}) async {
|
|
||||||
assert(context != null);
|
|
||||||
assert(initialDate != null);
|
|
||||||
assert(firstDate != null);
|
|
||||||
assert(lastDate != null);
|
|
||||||
initialDate = utils.dateOnly(initialDate);
|
|
||||||
firstDate = utils.dateOnly(firstDate);
|
|
||||||
lastDate = utils.dateOnly(lastDate);
|
|
||||||
assert(
|
|
||||||
!lastDate.isBefore(firstDate),
|
|
||||||
'lastDate $lastDate must be on or after firstDate $firstDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
!initialDate.isBefore(firstDate),
|
|
||||||
'initialDate $initialDate must be on or after firstDate $firstDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
!initialDate.isAfter(lastDate),
|
|
||||||
'initialDate $initialDate must be on or before lastDate $lastDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
selectableDayPredicate == null || selectableDayPredicate(initialDate),
|
|
||||||
'Provided initialDate $initialDate must satisfy provided selectableDayPredicate.'
|
|
||||||
);
|
|
||||||
assert(initialEntryMode != null);
|
|
||||||
assert(useRootNavigator != null);
|
|
||||||
assert(initialDatePickerMode != null);
|
|
||||||
assert(debugCheckHasMaterialLocalizations(context));
|
|
||||||
|
|
||||||
Widget dialog = _DatePickerDialog(
|
|
||||||
initialDate: initialDate,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
currentDate: currentDate,
|
|
||||||
initialEntryMode: initialEntryMode,
|
|
||||||
selectableDayPredicate: selectableDayPredicate,
|
|
||||||
helpText: helpText,
|
|
||||||
cancelText: cancelText,
|
|
||||||
confirmText: confirmText,
|
|
||||||
initialCalendarMode: initialDatePickerMode,
|
|
||||||
errorFormatText: errorFormatText,
|
|
||||||
errorInvalidText: errorInvalidText,
|
|
||||||
fieldHintText: fieldHintText,
|
|
||||||
fieldLabelText: fieldLabelText,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (textDirection != null) {
|
|
||||||
dialog = Directionality(
|
|
||||||
textDirection: textDirection,
|
|
||||||
child: dialog,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale != null) {
|
|
||||||
dialog = Localizations.override(
|
|
||||||
context: context,
|
|
||||||
locale: locale,
|
|
||||||
child: dialog,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return showDialog<DateTime>(
|
|
||||||
context: context,
|
|
||||||
useRootNavigator: useRootNavigator,
|
|
||||||
routeSettings: routeSettings,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return builder == null ? dialog : builder(context, dialog);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DatePickerDialog extends StatefulWidget {
|
|
||||||
_DatePickerDialog({
|
|
||||||
Key? key,
|
|
||||||
required DateTime initialDate,
|
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
DateTime? currentDate,
|
|
||||||
this.initialEntryMode = DatePickerEntryMode.calendar,
|
|
||||||
this.selectableDayPredicate,
|
|
||||||
this.cancelText,
|
|
||||||
this.confirmText,
|
|
||||||
this.helpText,
|
|
||||||
this.initialCalendarMode = DatePickerMode.day,
|
|
||||||
this.errorFormatText,
|
|
||||||
this.errorInvalidText,
|
|
||||||
this.fieldHintText,
|
|
||||||
this.fieldLabelText,
|
|
||||||
}) : assert(initialDate != null),
|
|
||||||
assert(firstDate != null),
|
|
||||||
assert(lastDate != null),
|
|
||||||
initialDate = utils.dateOnly(initialDate),
|
|
||||||
firstDate = utils.dateOnly(firstDate),
|
|
||||||
lastDate = utils.dateOnly(lastDate),
|
|
||||||
currentDate = utils.dateOnly(currentDate ?? DateTime.now()),
|
|
||||||
assert(initialEntryMode != null),
|
|
||||||
assert(initialCalendarMode != null),
|
|
||||||
super(key: key) {
|
|
||||||
assert(
|
|
||||||
!this.lastDate.isBefore(this.firstDate),
|
|
||||||
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
!this.initialDate.isBefore(this.firstDate),
|
|
||||||
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
!this.initialDate.isAfter(this.lastDate),
|
|
||||||
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
selectableDayPredicate == null || selectableDayPredicate!(this.initialDate),
|
|
||||||
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The initially selected [DateTime] that the picker should display.
|
|
||||||
final DateTime initialDate;
|
|
||||||
|
|
||||||
/// The earliest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime firstDate;
|
|
||||||
|
|
||||||
/// The latest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime lastDate;
|
|
||||||
|
|
||||||
/// The [DateTime] representing today. It will be highlighted in the day grid.
|
|
||||||
final DateTime currentDate;
|
|
||||||
|
|
||||||
final DatePickerEntryMode initialEntryMode;
|
|
||||||
|
|
||||||
/// Function to provide full control over which [DateTime] can be selected.
|
|
||||||
final SelectableDayPredicate? selectableDayPredicate;
|
|
||||||
|
|
||||||
/// The text that is displayed on the cancel button.
|
|
||||||
final String? cancelText;
|
|
||||||
|
|
||||||
/// The text that is displayed on the confirm button.
|
|
||||||
final String? confirmText;
|
|
||||||
|
|
||||||
/// The text that is displayed at the top of the header.
|
|
||||||
///
|
|
||||||
/// This is used to indicate to the user what they are selecting a date for.
|
|
||||||
final String? helpText;
|
|
||||||
|
|
||||||
/// The initial display of the calendar picker.
|
|
||||||
final DatePickerMode initialCalendarMode;
|
|
||||||
|
|
||||||
final String? errorFormatText;
|
|
||||||
|
|
||||||
final String? errorInvalidText;
|
|
||||||
|
|
||||||
final String? fieldHintText;
|
|
||||||
|
|
||||||
final String? fieldLabelText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_DatePickerDialogState createState() => _DatePickerDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DatePickerDialogState extends State<_DatePickerDialog> {
|
|
||||||
|
|
||||||
late DatePickerEntryMode _entryMode;
|
|
||||||
late DateTime _selectedDate;
|
|
||||||
late bool _autoValidate;
|
|
||||||
final GlobalKey _calendarPickerKey = GlobalKey();
|
|
||||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_entryMode = widget.initialEntryMode;
|
|
||||||
_selectedDate = widget.initialDate;
|
|
||||||
_autoValidate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOk() {
|
|
||||||
if (_entryMode == DatePickerEntryMode.input) {
|
|
||||||
final FormState form = _formKey.currentState!;
|
|
||||||
if (!form.validate()) {
|
|
||||||
setState(() => _autoValidate = true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form.save();
|
|
||||||
}
|
|
||||||
Navigator.pop(context, _selectedDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleCancel() {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEntryModeToggle() {
|
|
||||||
setState(() {
|
|
||||||
switch (_entryMode) {
|
|
||||||
case DatePickerEntryMode.calendar:
|
|
||||||
_autoValidate = false;
|
|
||||||
_entryMode = DatePickerEntryMode.input;
|
|
||||||
break;
|
|
||||||
case DatePickerEntryMode.input:
|
|
||||||
_formKey.currentState!.save();
|
|
||||||
_entryMode = DatePickerEntryMode.calendar;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleDateChanged(DateTime date) {
|
|
||||||
setState(() => _selectedDate = date);
|
|
||||||
}
|
|
||||||
|
|
||||||
Size _dialogSize(BuildContext context) {
|
|
||||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
|
||||||
switch (_entryMode) {
|
|
||||||
case DatePickerEntryMode.calendar:
|
|
||||||
switch (orientation) {
|
|
||||||
case Orientation.portrait:
|
|
||||||
return _calendarPortraitDialogSize;
|
|
||||||
case Orientation.landscape:
|
|
||||||
return _calendarLandscapeDialogSize;
|
|
||||||
}
|
|
||||||
case DatePickerEntryMode.input:
|
|
||||||
switch (orientation) {
|
|
||||||
case Orientation.portrait:
|
|
||||||
return _inputPortraitDialogSize;
|
|
||||||
case Orientation.landscape:
|
|
||||||
return _inputLandscapeDialogSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final Map<LogicalKeySet, Intent> _formShortcutMap = <LogicalKeySet, Intent>{
|
|
||||||
// Pressing enter on the field will move focus to the next field or control.
|
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const NextFocusIntent(),
|
|
||||||
};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
|
||||||
final TextTheme textTheme = theme.textTheme;
|
|
||||||
// Constrain the textScaleFactor to the largest supported value to prevent
|
|
||||||
// layout issues.
|
|
||||||
final double textScaleFactor = math.min(MediaQuery.of(context).textScaleFactor, 1.3);
|
|
||||||
|
|
||||||
final String dateText = localizations.formatMediumDate(_selectedDate);
|
|
||||||
final Color dateColor = colorScheme.brightness == Brightness.light
|
|
||||||
? colorScheme.onPrimary
|
|
||||||
: colorScheme.onSurface;
|
|
||||||
final TextStyle? dateStyle = orientation == Orientation.landscape
|
|
||||||
? textTheme.headline5?.copyWith(color: dateColor)
|
|
||||||
: textTheme.headline4?.copyWith(color: dateColor);
|
|
||||||
|
|
||||||
final Widget actions = Container(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
constraints: const BoxConstraints(minHeight: 52.0),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: OverflowBar(
|
|
||||||
spacing: 8,
|
|
||||||
children: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: Text(widget.cancelText ?? localizations.cancelButtonLabel),
|
|
||||||
onPressed: _handleCancel,
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: Text(widget.confirmText ?? localizations.okButtonLabel),
|
|
||||||
onPressed: _handleOk,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Widget picker;
|
|
||||||
final IconData entryModeIcon;
|
|
||||||
final String entryModeTooltip;
|
|
||||||
switch (_entryMode) {
|
|
||||||
case DatePickerEntryMode.calendar:
|
|
||||||
picker = CalendarDatePicker(
|
|
||||||
key: _calendarPickerKey,
|
|
||||||
initialDate: _selectedDate,
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
currentDate: widget.currentDate,
|
|
||||||
onDateChanged: _handleDateChanged,
|
|
||||||
selectableDayPredicate: widget.selectableDayPredicate,
|
|
||||||
initialCalendarMode: widget.initialCalendarMode,
|
|
||||||
);
|
|
||||||
entryModeIcon = Icons.edit;
|
|
||||||
entryModeTooltip = localizations.inputDateModeButtonLabel;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DatePickerEntryMode.input:
|
|
||||||
picker = Form(
|
|
||||||
key: _formKey,
|
|
||||||
autovalidate: _autoValidate,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
height: orientation == Orientation.portrait ? _inputFormPortraitHeight : _inputFormLandscapeHeight,
|
|
||||||
child: Shortcuts(
|
|
||||||
shortcuts: _formShortcutMap,
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
const Spacer(),
|
|
||||||
InputDatePickerFormField(
|
|
||||||
initialDate: _selectedDate,
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
onDateSubmitted: _handleDateChanged,
|
|
||||||
onDateSaved: _handleDateChanged,
|
|
||||||
selectableDayPredicate: widget.selectableDayPredicate,
|
|
||||||
errorFormatText: widget.errorFormatText,
|
|
||||||
errorInvalidText: widget.errorInvalidText,
|
|
||||||
fieldHintText: widget.fieldHintText,
|
|
||||||
fieldLabelText: widget.fieldLabelText,
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
entryModeIcon = Icons.calendar_today;
|
|
||||||
entryModeTooltip = localizations.calendarModeButtonLabel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Widget header = DatePickerHeader(
|
|
||||||
helpText: widget.helpText ?? localizations.datePickerHelpText,
|
|
||||||
titleText: dateText,
|
|
||||||
titleStyle: dateStyle,
|
|
||||||
orientation: orientation,
|
|
||||||
isShort: orientation == Orientation.landscape,
|
|
||||||
icon: entryModeIcon,
|
|
||||||
iconTooltip: entryModeTooltip,
|
|
||||||
onIconPressed: _handleEntryModeToggle,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Size dialogSize = _dialogSize(context) * textScaleFactor;
|
|
||||||
return Dialog(
|
|
||||||
child: AnimatedContainer(
|
|
||||||
width: dialogSize.width,
|
|
||||||
height: dialogSize.height,
|
|
||||||
duration: _dialogSizeAnimationDuration,
|
|
||||||
curve: Curves.easeIn,
|
|
||||||
child: MediaQuery(
|
|
||||||
data: MediaQuery.of(context).copyWith(
|
|
||||||
textScaleFactor: textScaleFactor,
|
|
||||||
),
|
|
||||||
child: Builder(builder: (BuildContext context) {
|
|
||||||
switch (orientation) {
|
|
||||||
case Orientation.portrait:
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
header,
|
|
||||||
Expanded(child: picker),
|
|
||||||
actions,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
case Orientation.landscape:
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
header,
|
|
||||||
Flexible(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(child: picker),
|
|
||||||
actions,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
// 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/widgets.dart';
|
|
||||||
|
|
||||||
import '../color_scheme.dart';
|
|
||||||
import '../icon_button.dart';
|
|
||||||
import '../material.dart';
|
|
||||||
import '../text_theme.dart';
|
|
||||||
import '../theme.dart';
|
|
||||||
|
|
||||||
// This is an internal implementation file. Even though there are public
|
|
||||||
// classes and functions defined here, they are only meant to be used by the
|
|
||||||
// date picker implementation and are not exported as part of the Material library.
|
|
||||||
// See pickers.dart for exactly what is considered part of the public API.
|
|
||||||
|
|
||||||
const double _datePickerHeaderLandscapeWidth = 152.0;
|
|
||||||
const double _datePickerHeaderPortraitHeight = 120.0;
|
|
||||||
const double _headerPaddingLandscape = 16.0;
|
|
||||||
|
|
||||||
/// Re-usable widget that displays the selected date (in large font) and the
|
|
||||||
/// help text above it.
|
|
||||||
///
|
|
||||||
/// These types include:
|
|
||||||
///
|
|
||||||
/// * Single Date picker with calendar mode.
|
|
||||||
/// * Single Date picker with manual input mode.
|
|
||||||
/// * Date Range picker with manual input mode.
|
|
||||||
///
|
|
||||||
/// [helpText], [orientation], [icon], [onIconPressed] are required and must be
|
|
||||||
/// non-null.
|
|
||||||
class DatePickerHeader extends StatelessWidget {
|
|
||||||
/// Creates a header for use in a date picker dialog.
|
|
||||||
const DatePickerHeader({
|
|
||||||
Key? key,
|
|
||||||
required this.helpText,
|
|
||||||
required this.titleText,
|
|
||||||
this.titleSemanticsLabel,
|
|
||||||
required this.titleStyle,
|
|
||||||
required this.orientation,
|
|
||||||
this.isShort = false,
|
|
||||||
required this.icon,
|
|
||||||
required this.iconTooltip,
|
|
||||||
required this.onIconPressed,
|
|
||||||
}) : assert(helpText != null),
|
|
||||||
assert(orientation != null),
|
|
||||||
assert(isShort != null),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// The text that is displayed at the top of the header.
|
|
||||||
///
|
|
||||||
/// This is used to indicate to the user what they are selecting a date for.
|
|
||||||
final String helpText;
|
|
||||||
|
|
||||||
/// The text that is displayed at the center of the header.
|
|
||||||
final String titleText;
|
|
||||||
|
|
||||||
/// The semantic label associated with the [titleText].
|
|
||||||
final String? titleSemanticsLabel;
|
|
||||||
|
|
||||||
/// The [TextStyle] that the title text is displayed with.
|
|
||||||
final TextStyle? titleStyle;
|
|
||||||
|
|
||||||
/// The orientation is used to decide how to layout its children.
|
|
||||||
final Orientation orientation;
|
|
||||||
|
|
||||||
/// Indicates the header is being displayed in a shorter/narrower context.
|
|
||||||
///
|
|
||||||
/// This will be used to tighten up the space between the help text and date
|
|
||||||
/// text if `true`. Additionally, it will use a smaller typography style if
|
|
||||||
/// `true`.
|
|
||||||
///
|
|
||||||
/// This is necessary for displaying the manual input mode in
|
|
||||||
/// landscape orientation, in order to account for the keyboard height.
|
|
||||||
final bool isShort;
|
|
||||||
|
|
||||||
/// The mode-switching icon that will be displayed in the lower right
|
|
||||||
/// in portrait, and lower left in landscape.
|
|
||||||
///
|
|
||||||
/// The available icons are described in [Icons].
|
|
||||||
final IconData icon;
|
|
||||||
|
|
||||||
/// The text that is displayed for the tooltip of the icon.
|
|
||||||
final String iconTooltip;
|
|
||||||
|
|
||||||
/// Callback when the user taps the icon in the header.
|
|
||||||
///
|
|
||||||
/// The picker will use this to toggle between entry modes.
|
|
||||||
final VoidCallback onIconPressed;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
|
||||||
final TextTheme textTheme = theme.textTheme;
|
|
||||||
|
|
||||||
// The header should use the primary color in light themes and surface color in dark
|
|
||||||
final bool isDark = colorScheme.brightness == Brightness.dark;
|
|
||||||
final Color primarySurfaceColor = isDark ? colorScheme.surface : colorScheme.primary;
|
|
||||||
final Color onPrimarySurfaceColor = isDark ? colorScheme.onSurface : colorScheme.onPrimary;
|
|
||||||
|
|
||||||
final TextStyle? helpStyle = textTheme.overline?.copyWith(
|
|
||||||
color: onPrimarySurfaceColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Text help = Text(
|
|
||||||
helpText,
|
|
||||||
style: helpStyle,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
|
||||||
final Text title = Text(
|
|
||||||
titleText,
|
|
||||||
semanticsLabel: titleSemanticsLabel ?? titleText,
|
|
||||||
style: titleStyle,
|
|
||||||
maxLines: orientation == Orientation.portrait ? 1 : 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
);
|
|
||||||
final IconButton icon = IconButton(
|
|
||||||
icon: Icon(this.icon),
|
|
||||||
color: onPrimarySurfaceColor,
|
|
||||||
tooltip: iconTooltip,
|
|
||||||
onPressed: onIconPressed,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (orientation) {
|
|
||||||
case Orientation.portrait:
|
|
||||||
return SizedBox(
|
|
||||||
height: _datePickerHeaderPortraitHeight,
|
|
||||||
child: Material(
|
|
||||||
color: primarySurfaceColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsetsDirectional.only(
|
|
||||||
start: 24,
|
|
||||||
end: 12,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
help,
|
|
||||||
const Flexible(child: SizedBox(height: 38)),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(child: title),
|
|
||||||
icon,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
case Orientation.landscape:
|
|
||||||
return SizedBox(
|
|
||||||
width: _datePickerHeaderLandscapeWidth,
|
|
||||||
child: Material(
|
|
||||||
color: primarySurfaceColor,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: _headerPaddingLandscape,
|
|
||||||
),
|
|
||||||
child: help,
|
|
||||||
),
|
|
||||||
SizedBox(height: isShort ? 16 : 56),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: _headerPaddingLandscape,
|
|
||||||
),
|
|
||||||
child: title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 4,
|
|
||||||
),
|
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,713 +0,0 @@
|
||||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import '../app_bar.dart';
|
|
||||||
import '../back_button.dart';
|
|
||||||
import '../color_scheme.dart';
|
|
||||||
import '../debug.dart';
|
|
||||||
import '../dialog.dart';
|
|
||||||
import '../dialog_theme.dart';
|
|
||||||
import '../icon_button.dart';
|
|
||||||
import '../icons.dart';
|
|
||||||
import '../material_localizations.dart';
|
|
||||||
import '../scaffold.dart';
|
|
||||||
import '../text_button.dart';
|
|
||||||
import '../text_theme.dart';
|
|
||||||
import '../theme.dart';
|
|
||||||
|
|
||||||
import 'calendar_date_range_picker.dart';
|
|
||||||
import 'date_picker_common.dart';
|
|
||||||
import 'date_picker_header.dart';
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
import 'input_date_range_picker.dart';
|
|
||||||
|
|
||||||
const Size _inputPortraitDialogSize = Size(330.0, 270.0);
|
|
||||||
const Size _inputLandscapeDialogSize = Size(496, 164.0);
|
|
||||||
const Duration _dialogSizeAnimationDuration = Duration(milliseconds: 200);
|
|
||||||
const double _inputFormPortraitHeight = 98.0;
|
|
||||||
const double _inputFormLandscapeHeight = 108.0;
|
|
||||||
|
|
||||||
/// Shows a full screen modal dialog containing a Material Design date range
|
|
||||||
/// picker.
|
|
||||||
///
|
|
||||||
/// The returned [Future] resolves to the [DateTimeRange] selected by the user
|
|
||||||
/// when the user saves their selection. If the user cancels the dialog, null is
|
|
||||||
/// returned.
|
|
||||||
///
|
|
||||||
/// If [initialDateRange] is non-null, then it will be used as the initially
|
|
||||||
/// selected date range. If it is provided, [initialDateRange.start] must be
|
|
||||||
/// before or on [initialDateRange.end].
|
|
||||||
///
|
|
||||||
/// The [firstDate] is the earliest allowable date. The [lastDate] is the latest
|
|
||||||
/// allowable date. Both must be non-null.
|
|
||||||
///
|
|
||||||
/// If an initial date range is provided, [initialDateRange.start]
|
|
||||||
/// and [initialDateRange.end] must both fall between or on [firstDate] and
|
|
||||||
/// [lastDate]. For all of these [DateTime] values, only their dates are
|
|
||||||
/// considered. Their time fields are ignored.
|
|
||||||
///
|
|
||||||
/// The [currentDate] represents the current day (i.e. today). This
|
|
||||||
/// date will be highlighted in the day grid. If null, the date of
|
|
||||||
/// `DateTime.now()` will be used.
|
|
||||||
///
|
|
||||||
/// An optional [initialEntryMode] argument can be used to display the date
|
|
||||||
/// picker in the [DatePickerEntryMode.calendar] (a scrollable calendar month
|
|
||||||
/// grid) or [DatePickerEntryMode.input] (two text input fields) mode.
|
|
||||||
/// It defaults to [DatePickerEntryMode.calendar] and must be non-null.
|
|
||||||
///
|
|
||||||
/// The following optional string parameters allow you to override the default
|
|
||||||
/// text used for various parts of the dialog:
|
|
||||||
///
|
|
||||||
/// * [helpText], the label displayed at the top of the dialog.
|
|
||||||
/// * [cancelText], the label on the cancel button for the text input mode.
|
|
||||||
/// * [confirmText],the label on the ok button for the text input mode.
|
|
||||||
/// * [saveText], the label on the save button for the fullscreen calendar
|
|
||||||
/// mode.
|
|
||||||
/// * [errorFormatText], the message used when an input text isn't in a proper
|
|
||||||
/// date format.
|
|
||||||
/// * [errorInvalidText], the message used when an input text isn't a
|
|
||||||
/// selectable date.
|
|
||||||
/// * [errorInvalidRangeText], the message used when the date range is
|
|
||||||
/// invalid (e.g. start date is after end date).
|
|
||||||
/// * [fieldStartHintText], the text used to prompt the user when no text has
|
|
||||||
/// been entered in the start field.
|
|
||||||
/// * [fieldEndHintText], the text used to prompt the user when no text has
|
|
||||||
/// been entered in the end field.
|
|
||||||
/// * [fieldStartLabelText], the label for the start date text input field.
|
|
||||||
/// * [fieldEndLabelText], the label for the end date text input field.
|
|
||||||
///
|
|
||||||
/// An optional [locale] argument can be used to set the locale for the date
|
|
||||||
/// picker. It defaults to the ambient locale provided by [Localizations].
|
|
||||||
///
|
|
||||||
/// An optional [textDirection] argument can be used to set the text direction
|
|
||||||
/// ([TextDirection.ltr] or [TextDirection.rtl]) for the date picker. It
|
|
||||||
/// defaults to the ambient text direction provided by [Directionality]. If both
|
|
||||||
/// [locale] and [textDirection] are non-null, [textDirection] overrides the
|
|
||||||
/// direction chosen for the [locale].
|
|
||||||
///
|
|
||||||
/// The [context], [useRootNavigator] and [routeSettings] arguments are passed
|
|
||||||
/// to [showDialog], the documentation for which discusses how it is used.
|
|
||||||
/// [context] and [useRootNavigator] must be non-null.
|
|
||||||
///
|
|
||||||
/// The [builder] parameter can be used to wrap the dialog widget
|
|
||||||
/// to add inherited widgets like [Theme].
|
|
||||||
///
|
|
||||||
/// See also:
|
|
||||||
///
|
|
||||||
/// * [showDatePicker], which shows a material design date picker used to
|
|
||||||
/// select a single date.
|
|
||||||
/// * [DateTimeRange], which is used to describe a date range.
|
|
||||||
///
|
|
||||||
Future<DateTimeRange?> showDateRangePicker({
|
|
||||||
required BuildContext context,
|
|
||||||
DateTimeRange? initialDateRange,
|
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
DateTime? currentDate,
|
|
||||||
DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar,
|
|
||||||
String? helpText,
|
|
||||||
String? cancelText,
|
|
||||||
String? confirmText,
|
|
||||||
String? saveText,
|
|
||||||
String? errorFormatText,
|
|
||||||
String? errorInvalidText,
|
|
||||||
String? errorInvalidRangeText,
|
|
||||||
String? fieldStartHintText,
|
|
||||||
String? fieldEndHintText,
|
|
||||||
String? fieldStartLabelText,
|
|
||||||
String? fieldEndLabelText,
|
|
||||||
Locale? locale,
|
|
||||||
bool useRootNavigator = true,
|
|
||||||
RouteSettings? routeSettings,
|
|
||||||
TextDirection? textDirection,
|
|
||||||
TransitionBuilder? builder,
|
|
||||||
}) async {
|
|
||||||
assert(context != null);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || (initialDateRange.start != null && initialDateRange.end != null),
|
|
||||||
'initialDateRange must be null or have non-null start and end dates.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || !initialDateRange.start.isAfter(initialDateRange.end),
|
|
||||||
'initialDateRange\'s start date must not be after it\'s end date.'
|
|
||||||
);
|
|
||||||
initialDateRange = initialDateRange == null ? null : utils.datesOnly(initialDateRange);
|
|
||||||
assert(firstDate != null);
|
|
||||||
firstDate = utils.dateOnly(firstDate);
|
|
||||||
assert(lastDate != null);
|
|
||||||
lastDate = utils.dateOnly(lastDate);
|
|
||||||
assert(
|
|
||||||
!lastDate.isBefore(firstDate),
|
|
||||||
'lastDate $lastDate must be on or after firstDate $firstDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || !initialDateRange.start.isBefore(firstDate),
|
|
||||||
'initialDateRange\'s start date must be on or after firstDate $firstDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || !initialDateRange.end.isBefore(firstDate),
|
|
||||||
'initialDateRange\'s end date must be on or after firstDate $firstDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || !initialDateRange.start.isAfter(lastDate),
|
|
||||||
'initialDateRange\'s start date must be on or before lastDate $lastDate.'
|
|
||||||
);
|
|
||||||
assert(
|
|
||||||
initialDateRange == null || !initialDateRange.end.isAfter(lastDate),
|
|
||||||
'initialDateRange\'s end date must be on or before lastDate $lastDate.'
|
|
||||||
);
|
|
||||||
currentDate = utils.dateOnly(currentDate ?? DateTime.now());
|
|
||||||
assert(initialEntryMode != null);
|
|
||||||
assert(useRootNavigator != null);
|
|
||||||
assert(debugCheckHasMaterialLocalizations(context));
|
|
||||||
|
|
||||||
Widget dialog = _DateRangePickerDialog(
|
|
||||||
initialDateRange: initialDateRange,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
currentDate: currentDate,
|
|
||||||
initialEntryMode: initialEntryMode,
|
|
||||||
helpText: helpText,
|
|
||||||
cancelText: cancelText,
|
|
||||||
confirmText: confirmText,
|
|
||||||
saveText: saveText,
|
|
||||||
errorFormatText: errorFormatText,
|
|
||||||
errorInvalidText: errorInvalidText,
|
|
||||||
errorInvalidRangeText: errorInvalidRangeText,
|
|
||||||
fieldStartHintText: fieldStartHintText,
|
|
||||||
fieldEndHintText: fieldEndHintText,
|
|
||||||
fieldStartLabelText: fieldStartLabelText,
|
|
||||||
fieldEndLabelText: fieldEndLabelText,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (textDirection != null) {
|
|
||||||
dialog = Directionality(
|
|
||||||
textDirection: textDirection,
|
|
||||||
child: dialog,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale != null) {
|
|
||||||
dialog = Localizations.override(
|
|
||||||
context: context,
|
|
||||||
locale: locale,
|
|
||||||
child: dialog,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return showDialog<DateTimeRange>(
|
|
||||||
context: context,
|
|
||||||
useRootNavigator: useRootNavigator,
|
|
||||||
routeSettings: routeSettings,
|
|
||||||
useSafeArea: false,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return builder == null ? dialog : builder(context, dialog);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DateRangePickerDialog extends StatefulWidget {
|
|
||||||
const _DateRangePickerDialog({
|
|
||||||
Key? key,
|
|
||||||
this.initialDateRange,
|
|
||||||
required this.firstDate,
|
|
||||||
required this.lastDate,
|
|
||||||
this.currentDate,
|
|
||||||
this.initialEntryMode = DatePickerEntryMode.calendar,
|
|
||||||
this.helpText,
|
|
||||||
this.cancelText,
|
|
||||||
this.confirmText,
|
|
||||||
this.saveText,
|
|
||||||
this.errorInvalidRangeText,
|
|
||||||
this.errorFormatText,
|
|
||||||
this.errorInvalidText,
|
|
||||||
this.fieldStartHintText,
|
|
||||||
this.fieldEndHintText,
|
|
||||||
this.fieldStartLabelText,
|
|
||||||
this.fieldEndLabelText,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final DateTimeRange? initialDateRange;
|
|
||||||
final DateTime firstDate;
|
|
||||||
final DateTime lastDate;
|
|
||||||
final DateTime? currentDate;
|
|
||||||
final DatePickerEntryMode initialEntryMode;
|
|
||||||
final String? cancelText;
|
|
||||||
final String? confirmText;
|
|
||||||
final String? saveText;
|
|
||||||
final String? helpText;
|
|
||||||
final String? errorInvalidRangeText;
|
|
||||||
final String? errorFormatText;
|
|
||||||
final String? errorInvalidText;
|
|
||||||
final String? fieldStartHintText;
|
|
||||||
final String? fieldEndHintText;
|
|
||||||
final String? fieldStartLabelText;
|
|
||||||
final String? fieldEndLabelText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_DateRangePickerDialogState createState() => _DateRangePickerDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DateRangePickerDialogState extends State<_DateRangePickerDialog> {
|
|
||||||
late DatePickerEntryMode _entryMode;
|
|
||||||
DateTime? _selectedStart;
|
|
||||||
DateTime? _selectedEnd;
|
|
||||||
late bool _autoValidate;
|
|
||||||
final GlobalKey _calendarPickerKey = GlobalKey();
|
|
||||||
final GlobalKey<InputDateRangePickerState> _inputPickerKey = GlobalKey<InputDateRangePickerState>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_selectedStart = widget.initialDateRange?.start;
|
|
||||||
_selectedEnd = widget.initialDateRange?.end;
|
|
||||||
_entryMode = widget.initialEntryMode;
|
|
||||||
_autoValidate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOk() {
|
|
||||||
if (_entryMode == DatePickerEntryMode.input) {
|
|
||||||
final InputDateRangePickerState picker = _inputPickerKey.currentState!;
|
|
||||||
if (!picker.validate()) {
|
|
||||||
setState(() {
|
|
||||||
_autoValidate = true;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final DateTimeRange? selectedRange = _hasSelectedDateRange
|
|
||||||
? DateTimeRange(start: _selectedStart!, end: _selectedEnd!)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
Navigator.pop(context, selectedRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleCancel() {
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEntryModeToggle() {
|
|
||||||
setState(() {
|
|
||||||
switch (_entryMode) {
|
|
||||||
case DatePickerEntryMode.calendar:
|
|
||||||
_autoValidate = false;
|
|
||||||
_entryMode = DatePickerEntryMode.input;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DatePickerEntryMode.input:
|
|
||||||
// Validate the range dates
|
|
||||||
if (_selectedStart != null &&
|
|
||||||
(_selectedStart!.isBefore(widget.firstDate) || _selectedStart!.isAfter(widget.lastDate))) {
|
|
||||||
_selectedStart = null;
|
|
||||||
// With no valid start date, having an end date makes no sense for the UI.
|
|
||||||
_selectedEnd = null;
|
|
||||||
}
|
|
||||||
if (_selectedEnd != null &&
|
|
||||||
(_selectedEnd!.isBefore(widget.firstDate) || _selectedEnd!.isAfter(widget.lastDate))) {
|
|
||||||
_selectedEnd = null;
|
|
||||||
}
|
|
||||||
// If invalid range (start after end), then just use the start date
|
|
||||||
if (_selectedStart != null && _selectedEnd != null && _selectedStart!.isAfter(_selectedEnd!)) {
|
|
||||||
_selectedEnd = null;
|
|
||||||
}
|
|
||||||
_entryMode = DatePickerEntryMode.calendar;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleStartDateChanged(DateTime? date) {
|
|
||||||
setState(() => _selectedStart = date);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEndDateChanged(DateTime? date) {
|
|
||||||
setState(() => _selectedEnd = date);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get _hasSelectedDateRange => _selectedStart != null && _selectedEnd != null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
|
||||||
final Orientation orientation = mediaQuery.orientation;
|
|
||||||
final double textScaleFactor = math.min(mediaQuery.textScaleFactor, 1.3);
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
|
|
||||||
final Widget contents;
|
|
||||||
final Size size;
|
|
||||||
ShapeBorder? shape;
|
|
||||||
final double elevation;
|
|
||||||
final EdgeInsets insetPadding;
|
|
||||||
switch (_entryMode) {
|
|
||||||
case DatePickerEntryMode.calendar:
|
|
||||||
contents = _CalendarRangePickerDialog(
|
|
||||||
key: _calendarPickerKey,
|
|
||||||
selectedStartDate: _selectedStart,
|
|
||||||
selectedEndDate: _selectedEnd,
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
currentDate: widget.currentDate,
|
|
||||||
onStartDateChanged: _handleStartDateChanged,
|
|
||||||
onEndDateChanged: _handleEndDateChanged,
|
|
||||||
onConfirm: _hasSelectedDateRange ? _handleOk : null,
|
|
||||||
onCancel: _handleCancel,
|
|
||||||
onToggleEntryMode: _handleEntryModeToggle,
|
|
||||||
confirmText: widget.saveText ?? localizations.saveButtonLabel,
|
|
||||||
helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
|
|
||||||
);
|
|
||||||
size = mediaQuery.size;
|
|
||||||
insetPadding = const EdgeInsets.all(0.0);
|
|
||||||
shape = const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.zero)
|
|
||||||
);
|
|
||||||
elevation = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DatePickerEntryMode.input:
|
|
||||||
contents = _InputDateRangePickerDialog(
|
|
||||||
selectedStartDate: _selectedStart,
|
|
||||||
selectedEndDate: _selectedEnd,
|
|
||||||
currentDate: widget.currentDate,
|
|
||||||
picker: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
height: orientation == Orientation.portrait
|
|
||||||
? _inputFormPortraitHeight
|
|
||||||
: _inputFormLandscapeHeight,
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
const Spacer(),
|
|
||||||
InputDateRangePicker(
|
|
||||||
key: _inputPickerKey,
|
|
||||||
initialStartDate: _selectedStart,
|
|
||||||
initialEndDate: _selectedEnd,
|
|
||||||
firstDate: widget.firstDate,
|
|
||||||
lastDate: widget.lastDate,
|
|
||||||
onStartDateChanged: _handleStartDateChanged,
|
|
||||||
onEndDateChanged: _handleEndDateChanged,
|
|
||||||
autofocus: true,
|
|
||||||
autovalidate: _autoValidate,
|
|
||||||
helpText: widget.helpText,
|
|
||||||
errorInvalidRangeText: widget.errorInvalidRangeText,
|
|
||||||
errorFormatText: widget.errorFormatText,
|
|
||||||
errorInvalidText: widget.errorInvalidText,
|
|
||||||
fieldStartHintText: widget.fieldStartHintText,
|
|
||||||
fieldEndHintText: widget.fieldEndHintText,
|
|
||||||
fieldStartLabelText: widget.fieldStartLabelText,
|
|
||||||
fieldEndLabelText: widget.fieldEndLabelText,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onConfirm: _handleOk,
|
|
||||||
onCancel: _handleCancel,
|
|
||||||
onToggleEntryMode: _handleEntryModeToggle,
|
|
||||||
confirmText: widget.confirmText ?? localizations.okButtonLabel,
|
|
||||||
cancelText: widget.cancelText ?? localizations.cancelButtonLabel,
|
|
||||||
helpText: widget.helpText ?? localizations.dateRangePickerHelpText,
|
|
||||||
);
|
|
||||||
final DialogTheme dialogTheme = Theme.of(context).dialogTheme;
|
|
||||||
size = orientation == Orientation.portrait ? _inputPortraitDialogSize : _inputLandscapeDialogSize;
|
|
||||||
insetPadding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0);
|
|
||||||
shape = dialogTheme.shape;
|
|
||||||
elevation = dialogTheme.elevation ?? 24;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Dialog(
|
|
||||||
child: AnimatedContainer(
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
duration: _dialogSizeAnimationDuration,
|
|
||||||
curve: Curves.easeIn,
|
|
||||||
child: MediaQuery(
|
|
||||||
data: MediaQuery.of(context).copyWith(
|
|
||||||
textScaleFactor: textScaleFactor,
|
|
||||||
),
|
|
||||||
child: Builder(builder: (BuildContext context) {
|
|
||||||
return contents;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
insetPadding: insetPadding,
|
|
||||||
shape: shape,
|
|
||||||
elevation: elevation,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarRangePickerDialog extends StatelessWidget {
|
|
||||||
const _CalendarRangePickerDialog({
|
|
||||||
Key? key,
|
|
||||||
required this.selectedStartDate,
|
|
||||||
required this.selectedEndDate,
|
|
||||||
required this.firstDate,
|
|
||||||
required this.lastDate,
|
|
||||||
required this.currentDate,
|
|
||||||
required this.onStartDateChanged,
|
|
||||||
required this.onEndDateChanged,
|
|
||||||
required this.onConfirm,
|
|
||||||
required this.onCancel,
|
|
||||||
required this.onToggleEntryMode,
|
|
||||||
required this.confirmText,
|
|
||||||
required this.helpText,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final DateTime? selectedStartDate;
|
|
||||||
final DateTime? selectedEndDate;
|
|
||||||
final DateTime firstDate;
|
|
||||||
final DateTime lastDate;
|
|
||||||
final DateTime? currentDate;
|
|
||||||
final ValueChanged<DateTime> onStartDateChanged;
|
|
||||||
final ValueChanged<DateTime?> onEndDateChanged;
|
|
||||||
final VoidCallback? onConfirm;
|
|
||||||
final VoidCallback? onCancel;
|
|
||||||
final VoidCallback? onToggleEntryMode;
|
|
||||||
final String confirmText;
|
|
||||||
final String helpText;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
|
||||||
final TextTheme textTheme = theme.textTheme;
|
|
||||||
final Color headerForeground = colorScheme.brightness == Brightness.light
|
|
||||||
? colorScheme.onPrimary
|
|
||||||
: colorScheme.onSurface;
|
|
||||||
final Color headerDisabledForeground = headerForeground.withOpacity(0.38);
|
|
||||||
final String startDateText = utils.formatRangeStartDate(localizations, selectedStartDate, selectedEndDate);
|
|
||||||
final String endDateText = utils.formatRangeEndDate(localizations, selectedStartDate, selectedEndDate, DateTime.now());
|
|
||||||
final TextStyle? headlineStyle = textTheme.headline5;
|
|
||||||
final TextStyle? startDateStyle = headlineStyle?.apply(
|
|
||||||
color: selectedStartDate != null ? headerForeground : headerDisabledForeground
|
|
||||||
);
|
|
||||||
final TextStyle? endDateStyle = headlineStyle?.apply(
|
|
||||||
color: selectedEndDate != null ? headerForeground : headerDisabledForeground
|
|
||||||
);
|
|
||||||
final TextStyle saveButtonStyle = textTheme.button!.apply(
|
|
||||||
color: onConfirm != null ? headerForeground : headerDisabledForeground
|
|
||||||
);
|
|
||||||
|
|
||||||
final IconButton entryModeIcon = IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
color: headerForeground,
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
tooltip: localizations.inputDateModeButtonLabel,
|
|
||||||
onPressed: onToggleEntryMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
left: false,
|
|
||||||
right: false,
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
leading: CloseButton(
|
|
||||||
onPressed: onCancel,
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
if (orientation == Orientation.landscape) entryModeIcon,
|
|
||||||
TextButton(
|
|
||||||
onPressed: onConfirm,
|
|
||||||
child: Text(confirmText, style: saveButtonStyle),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
],
|
|
||||||
bottom: PreferredSize(
|
|
||||||
child: Row(children: <Widget>[
|
|
||||||
SizedBox(width: MediaQuery.of(context).size.width < 360 ? 42 : 72),
|
|
||||||
Expanded(
|
|
||||||
child: Semantics(
|
|
||||||
label: '$helpText $startDateText to $endDateText',
|
|
||||||
excludeSemantics: true,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
helpText,
|
|
||||||
style: textTheme.overline!.apply(
|
|
||||||
color: headerForeground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
startDateText,
|
|
||||||
style: startDateStyle,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
Text(' – ', style: startDateStyle,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
endDateText,
|
|
||||||
style: endDateStyle,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (orientation == Orientation.portrait)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: entryModeIcon,
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
preferredSize: const Size(double.infinity, 64),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: CalendarDateRangePicker(
|
|
||||||
initialStartDate: selectedStartDate,
|
|
||||||
initialEndDate: selectedEndDate,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
currentDate: currentDate,
|
|
||||||
onStartDateChanged: onStartDateChanged,
|
|
||||||
onEndDateChanged: onEndDateChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _InputDateRangePickerDialog extends StatelessWidget {
|
|
||||||
const _InputDateRangePickerDialog({
|
|
||||||
Key? key,
|
|
||||||
required this.selectedStartDate,
|
|
||||||
required this.selectedEndDate,
|
|
||||||
required this.currentDate,
|
|
||||||
required this.picker,
|
|
||||||
required this.onConfirm,
|
|
||||||
required this.onCancel,
|
|
||||||
required this.onToggleEntryMode,
|
|
||||||
required this.confirmText,
|
|
||||||
required this.cancelText,
|
|
||||||
required this.helpText,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final DateTime? selectedStartDate;
|
|
||||||
final DateTime? selectedEndDate;
|
|
||||||
final DateTime? currentDate;
|
|
||||||
final Widget picker;
|
|
||||||
final VoidCallback onConfirm;
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
final VoidCallback onToggleEntryMode;
|
|
||||||
final String? confirmText;
|
|
||||||
final String? cancelText;
|
|
||||||
final String? helpText;
|
|
||||||
|
|
||||||
String _formatDateRange(BuildContext context, DateTime? start, DateTime? end, DateTime now) {
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final String startText = utils.formatRangeStartDate(localizations, start, end);
|
|
||||||
final String endText = utils.formatRangeEndDate(localizations, start, end, now);
|
|
||||||
if (start == null || end == null) {
|
|
||||||
return localizations.unspecifiedDateRange;
|
|
||||||
}
|
|
||||||
if (Directionality.of(context) == TextDirection.ltr) {
|
|
||||||
return '$startText – $endText';
|
|
||||||
} else {
|
|
||||||
return '$endText – $startText';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final ColorScheme colorScheme = theme.colorScheme;
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final Orientation orientation = MediaQuery.of(context).orientation;
|
|
||||||
final TextTheme textTheme = theme.textTheme;
|
|
||||||
|
|
||||||
final Color dateColor = colorScheme.brightness == Brightness.light
|
|
||||||
? colorScheme.onPrimary
|
|
||||||
: colorScheme.onSurface;
|
|
||||||
final TextStyle? dateStyle = orientation == Orientation.landscape
|
|
||||||
? textTheme.headline5?.apply(color: dateColor)
|
|
||||||
: textTheme.headline4?.apply(color: dateColor);
|
|
||||||
final String dateText = _formatDateRange(context, selectedStartDate, selectedEndDate, currentDate!);
|
|
||||||
final String semanticDateText = selectedStartDate != null && selectedEndDate != null
|
|
||||||
? '${localizations.formatMediumDate(selectedStartDate!)} – ${localizations.formatMediumDate(selectedEndDate!)}'
|
|
||||||
: '';
|
|
||||||
|
|
||||||
final Widget header = DatePickerHeader(
|
|
||||||
helpText: helpText ?? localizations.dateRangePickerHelpText,
|
|
||||||
titleText: dateText,
|
|
||||||
titleSemanticsLabel: semanticDateText,
|
|
||||||
titleStyle: dateStyle,
|
|
||||||
orientation: orientation,
|
|
||||||
isShort: orientation == Orientation.landscape,
|
|
||||||
icon: Icons.calendar_today,
|
|
||||||
iconTooltip: localizations.calendarModeButtonLabel,
|
|
||||||
onIconPressed: onToggleEntryMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
final Widget actions = Container(
|
|
||||||
alignment: AlignmentDirectional.centerEnd,
|
|
||||||
constraints: const BoxConstraints(minHeight: 52.0),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: OverflowBar(
|
|
||||||
spacing: 8,
|
|
||||||
children: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: Text(cancelText ?? localizations.cancelButtonLabel),
|
|
||||||
onPressed: onCancel,
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: Text(confirmText ?? localizations.okButtonLabel),
|
|
||||||
onPressed: onConfirm,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (orientation) {
|
|
||||||
case Orientation.portrait:
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
header,
|
|
||||||
Expanded(child: picker),
|
|
||||||
actions,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
case Orientation.landscape:
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
header,
|
|
||||||
Flexible(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(child: picker),
|
|
||||||
actions,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Common date utility functions used by the date picker implementation
|
|
||||||
|
|
||||||
// This is an internal implementation file. Even though there are public
|
|
||||||
// classes and functions defined here, they are only meant to be used by the
|
|
||||||
// date picker implementation and are not exported as part of the Material library.
|
|
||||||
// See pickers.dart for exactly what is considered part of the public API.
|
|
||||||
|
|
||||||
import '../material_localizations.dart';
|
|
||||||
|
|
||||||
import 'date_picker_common.dart';
|
|
||||||
|
|
||||||
/// Returns a [DateTime] with just the date of the original, but no time set.
|
|
||||||
DateTime dateOnly(DateTime date) {
|
|
||||||
return DateTime(date.year, date.month, date.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [DateTimeRange] with the dates of the original without any times set.
|
|
||||||
DateTimeRange datesOnly(DateTimeRange range) {
|
|
||||||
return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the two [DateTime] objects have the same day, month, and
|
|
||||||
/// year, or are both null.
|
|
||||||
bool isSameDay(DateTime? dateA, DateTime? dateB) {
|
|
||||||
return
|
|
||||||
dateA?.year == dateB?.year &&
|
|
||||||
dateA?.month == dateB?.month &&
|
|
||||||
dateA?.day == dateB?.day;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the two [DateTime] objects have the same month, and
|
|
||||||
/// year, or are both null.
|
|
||||||
bool isSameMonth(DateTime? dateA, DateTime? dateB) {
|
|
||||||
return
|
|
||||||
dateA?.year == dateB?.year &&
|
|
||||||
dateA?.month == dateB?.month;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Determines the number of months between two [DateTime] objects.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
/// ```
|
|
||||||
/// DateTime date1 = DateTime(year: 2019, month: 6, day: 15);
|
|
||||||
/// DateTime date2 = DateTime(year: 2020, month: 1, day: 15);
|
|
||||||
/// int delta = monthDelta(date1, date2);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The value for `delta` would be `7`.
|
|
||||||
int monthDelta(DateTime startDate, DateTime endDate) {
|
|
||||||
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [DateTime] with the added number of months and truncates any day
|
|
||||||
/// and time information.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
/// ```
|
|
||||||
/// DateTime date = DateTime(year: 2019, month: 1, day: 15);
|
|
||||||
/// DateTime futureDate = _addMonthsToMonthDate(date, 3);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// `date` would be January 15, 2019.
|
|
||||||
/// `futureDate` would be April 1, 2019 since it adds 3 months and truncates
|
|
||||||
/// any additional date information.
|
|
||||||
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
|
|
||||||
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [DateTime] with the added number of days and no time set.
|
|
||||||
DateTime addDaysToDate(DateTime date, int days) {
|
|
||||||
return DateTime(date.year, date.month, date.day + days);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Computes the offset from the first day of the week that the first day of
|
|
||||||
/// the [month] falls on.
|
|
||||||
///
|
|
||||||
/// For example, September 1, 2017 falls on a Friday, which in the calendar
|
|
||||||
/// localized for United States English appears as:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// S M T W T F S
|
|
||||||
/// _ _ _ _ _ 1 2
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The offset for the first day of the months is the number of leading blanks
|
|
||||||
/// in the calendar, i.e. 5.
|
|
||||||
///
|
|
||||||
/// The same date localized for the Russian calendar has a different offset,
|
|
||||||
/// because the first day of week is Monday rather than Sunday:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// M T W T F S S
|
|
||||||
/// _ _ _ _ 1 2 3
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// So the offset is 4, rather than 5.
|
|
||||||
///
|
|
||||||
/// This code consolidates the following:
|
|
||||||
///
|
|
||||||
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
|
|
||||||
/// falling on Monday.
|
|
||||||
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
|
|
||||||
/// into the [MaterialLocalizations.narrowWeekdays] list.
|
|
||||||
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
|
|
||||||
/// days of week, always starting with Sunday and ending with Saturday.
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 0-based start of week depending on the locale, with 0 representing Sunday.
|
|
||||||
int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex;
|
|
||||||
|
|
||||||
// firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with
|
|
||||||
// weekdayFromMonday.
|
|
||||||
firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7;
|
|
||||||
|
|
||||||
// Number of days between the first day of week appearing on the calendar,
|
|
||||||
// and the day corresponding to the first of the month.
|
|
||||||
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
int getDaysInMonth(int year, int month) {
|
|
||||||
if (month == DateTime.february) {
|
|
||||||
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) ||
|
|
||||||
(year % 400 == 0);
|
|
||||||
if (isLeapYear)
|
|
||||||
return 29;
|
|
||||||
return 28;
|
|
||||||
}
|
|
||||||
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
||||||
return daysInMonth[month - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a locale-appropriate string to describe the start of a date range.
|
|
||||||
///
|
|
||||||
/// If `startDate` is null, then it defaults to 'Start Date', otherwise if it
|
|
||||||
/// is in the same year as the `endDate` then it will use the short month
|
|
||||||
/// day format (i.e. 'Jan 21'). Otherwise it will return the short date format
|
|
||||||
/// (i.e. 'Jan 21, 2020').
|
|
||||||
String formatRangeStartDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate) {
|
|
||||||
return startDate == null
|
|
||||||
? localizations.dateRangeStartLabel
|
|
||||||
: (endDate == null || startDate.year == endDate.year)
|
|
||||||
? localizations.formatShortMonthDay(startDate)
|
|
||||||
: localizations.formatShortDate(startDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an locale-appropriate string to describe the end of a date range.
|
|
||||||
///
|
|
||||||
/// If `endDate` is null, then it defaults to 'End Date', otherwise if it
|
|
||||||
/// is in the same year as the `startDate` and the `currentDate` then it will
|
|
||||||
/// just use the short month day format (i.e. 'Jan 21'), otherwise it will
|
|
||||||
/// include the year (i.e. 'Jan 21, 2020').
|
|
||||||
String formatRangeEndDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate, DateTime currentDate) {
|
|
||||||
return endDate == null
|
|
||||||
? localizations.dateRangeEndLabel
|
|
||||||
: (startDate != null && startDate.year == endDate.year && startDate.year == currentDate.year)
|
|
||||||
? localizations.formatShortMonthDay(endDate)
|
|
||||||
: localizations.formatShortDate(endDate);
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
// 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/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import '../input_border.dart';
|
|
||||||
import '../input_decorator.dart';
|
|
||||||
import '../material_localizations.dart';
|
|
||||||
import '../text_field.dart';
|
|
||||||
import '../theme.dart';
|
|
||||||
|
|
||||||
import 'date_utils.dart' as utils;
|
|
||||||
|
|
||||||
/// Provides a pair of text fields that allow the user to enter the start and
|
|
||||||
/// end dates that represent a range of dates.
|
|
||||||
//
|
|
||||||
// This is not publicly exported (see pickers.dart), as it is just an
|
|
||||||
// internal component used by [showDateRangePicker].
|
|
||||||
class InputDateRangePicker extends StatefulWidget {
|
|
||||||
/// Creates a row with two text fields configured to accept the start and end dates
|
|
||||||
/// of a date range.
|
|
||||||
InputDateRangePicker({
|
|
||||||
Key? key,
|
|
||||||
DateTime? initialStartDate,
|
|
||||||
DateTime? initialEndDate,
|
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
required this.onStartDateChanged,
|
|
||||||
required this.onEndDateChanged,
|
|
||||||
this.helpText,
|
|
||||||
this.errorFormatText,
|
|
||||||
this.errorInvalidText,
|
|
||||||
this.errorInvalidRangeText,
|
|
||||||
this.fieldStartHintText,
|
|
||||||
this.fieldEndHintText,
|
|
||||||
this.fieldStartLabelText,
|
|
||||||
this.fieldEndLabelText,
|
|
||||||
this.autofocus = false,
|
|
||||||
this.autovalidate = false,
|
|
||||||
}) : initialStartDate = initialStartDate == null ? null : utils.dateOnly(initialStartDate),
|
|
||||||
initialEndDate = initialEndDate == null ? null : utils.dateOnly(initialEndDate),
|
|
||||||
assert(firstDate != null),
|
|
||||||
firstDate = utils.dateOnly(firstDate),
|
|
||||||
assert(lastDate != null),
|
|
||||||
lastDate = utils.dateOnly(lastDate),
|
|
||||||
assert(firstDate != null),
|
|
||||||
assert(lastDate != null),
|
|
||||||
assert(autofocus != null),
|
|
||||||
assert(autovalidate != null),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// The [DateTime] that represents the start of the initial date range selection.
|
|
||||||
final DateTime? initialStartDate;
|
|
||||||
|
|
||||||
/// The [DateTime] that represents the end of the initial date range selection.
|
|
||||||
final DateTime? initialEndDate;
|
|
||||||
|
|
||||||
/// The earliest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime firstDate;
|
|
||||||
|
|
||||||
/// The latest allowable [DateTime] that the user can select.
|
|
||||||
final DateTime lastDate;
|
|
||||||
|
|
||||||
/// Called when the user changes the start date of the selected range.
|
|
||||||
final ValueChanged<DateTime?>? onStartDateChanged;
|
|
||||||
|
|
||||||
/// Called when the user changes the end date of the selected range.
|
|
||||||
final ValueChanged<DateTime?>? onEndDateChanged;
|
|
||||||
|
|
||||||
/// The text that is displayed at the top of the header.
|
|
||||||
///
|
|
||||||
/// This is used to indicate to the user what they are selecting a date for.
|
|
||||||
final String? helpText;
|
|
||||||
|
|
||||||
/// Error text used to indicate the text in a field is not a valid date.
|
|
||||||
final String? errorFormatText;
|
|
||||||
|
|
||||||
/// Error text used to indicate the date in a field is not in the valid range
|
|
||||||
/// of [firstDate] - [lastDate].
|
|
||||||
final String? errorInvalidText;
|
|
||||||
|
|
||||||
/// Error text used to indicate the dates given don't form a valid date
|
|
||||||
/// range (i.e. the start date is after the end date).
|
|
||||||
final String? errorInvalidRangeText;
|
|
||||||
|
|
||||||
/// Hint text shown when the start date field is empty.
|
|
||||||
final String? fieldStartHintText;
|
|
||||||
|
|
||||||
/// Hint text shown when the end date field is empty.
|
|
||||||
final String? fieldEndHintText;
|
|
||||||
|
|
||||||
/// Label used for the start date field.
|
|
||||||
final String? fieldStartLabelText;
|
|
||||||
|
|
||||||
/// Label used for the end date field.
|
|
||||||
final String? fieldEndLabelText;
|
|
||||||
|
|
||||||
/// {@macro flutter.widgets.editableText.autofocus}
|
|
||||||
final bool autofocus;
|
|
||||||
|
|
||||||
/// If true, this the date fields will validate and update their error text
|
|
||||||
/// immediately after every change. Otherwise, you must call
|
|
||||||
/// [InputDateRangePickerState.validate] to validate.
|
|
||||||
final bool autovalidate;
|
|
||||||
|
|
||||||
@override
|
|
||||||
InputDateRangePickerState createState() => InputDateRangePickerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current state of an [InputDateRangePicker]. Can be used to
|
|
||||||
/// [validate] the date field entries.
|
|
||||||
class InputDateRangePickerState extends State<InputDateRangePicker> {
|
|
||||||
late String _startInputText;
|
|
||||||
late String _endInputText;
|
|
||||||
DateTime? _startDate;
|
|
||||||
DateTime? _endDate;
|
|
||||||
late TextEditingController _startController;
|
|
||||||
late TextEditingController _endController;
|
|
||||||
String? _startErrorText;
|
|
||||||
String? _endErrorText;
|
|
||||||
bool _autoSelected = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_startDate = widget.initialStartDate;
|
|
||||||
_startController = TextEditingController();
|
|
||||||
_endDate = widget.initialEndDate;
|
|
||||||
_endController = TextEditingController();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_startController.dispose();
|
|
||||||
_endController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
if (_startDate != null) {
|
|
||||||
_startInputText = localizations.formatCompactDate(_startDate!);
|
|
||||||
final bool selectText = widget.autofocus && !_autoSelected;
|
|
||||||
_updateController(_startController, _startInputText, selectText);
|
|
||||||
_autoSelected = selectText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_endDate != null) {
|
|
||||||
_endInputText = localizations.formatCompactDate(_endDate!);
|
|
||||||
_updateController(_endController, _endInputText, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates that the text in the start and end fields represent a valid
|
|
||||||
/// date range.
|
|
||||||
///
|
|
||||||
/// Will return true if the range is valid. If not, it will
|
|
||||||
/// return false and display an appropriate error message under one of the
|
|
||||||
/// text fields.
|
|
||||||
bool validate() {
|
|
||||||
String? startError = _validateDate(_startDate);
|
|
||||||
final String? endError = _validateDate(_endDate);
|
|
||||||
if (startError == null && endError == null) {
|
|
||||||
if (_startDate!.isAfter(_endDate!)) {
|
|
||||||
startError = widget.errorInvalidRangeText ?? MaterialLocalizations.of(context).invalidDateRangeLabel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_startErrorText = startError;
|
|
||||||
_endErrorText = endError;
|
|
||||||
});
|
|
||||||
return startError == null && endError == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? _parseDate(String? text) {
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
return localizations.parseCompactDate(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _validateDate(DateTime? date) {
|
|
||||||
if (date == null) {
|
|
||||||
return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel;
|
|
||||||
} else if (date.isBefore(widget.firstDate) || date.isAfter(widget.lastDate)) {
|
|
||||||
return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateController(TextEditingController controller, String text, bool selectText) {
|
|
||||||
TextEditingValue textEditingValue = controller.value.copyWith(text: text);
|
|
||||||
if (selectText) {
|
|
||||||
textEditingValue = textEditingValue.copyWith(selection: TextSelection(
|
|
||||||
baseOffset: 0,
|
|
||||||
extentOffset: text.length,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
controller.value = textEditingValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleStartChanged(String text) {
|
|
||||||
setState(() {
|
|
||||||
_startInputText = text;
|
|
||||||
_startDate = _parseDate(text);
|
|
||||||
widget.onStartDateChanged?.call(_startDate);
|
|
||||||
});
|
|
||||||
if (widget.autovalidate) {
|
|
||||||
validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleEndChanged(String text) {
|
|
||||||
setState(() {
|
|
||||||
_endInputText = text;
|
|
||||||
_endDate = _parseDate(text);
|
|
||||||
widget.onEndDateChanged?.call(_endDate);
|
|
||||||
});
|
|
||||||
if (widget.autovalidate) {
|
|
||||||
validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _startController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: inputTheme.border ?? const UnderlineInputBorder(),
|
|
||||||
filled: inputTheme.filled,
|
|
||||||
hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
|
|
||||||
labelText: widget.fieldStartLabelText ?? localizations.dateRangeStartLabel,
|
|
||||||
errorText: _startErrorText,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.datetime,
|
|
||||||
onChanged: _handleStartChanged,
|
|
||||||
autofocus: widget.autofocus,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _endController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: inputTheme.border ?? const UnderlineInputBorder(),
|
|
||||||
filled: inputTheme.filled,
|
|
||||||
hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
|
|
||||||
labelText: widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
|
|
||||||
errorText: _endErrorText,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.datetime,
|
|
||||||
onChanged: _handleEndChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Date Picker public API
|
|
||||||
export 'calendar_date_picker.dart' show CalendarDatePicker;
|
|
||||||
export 'date_picker_common.dart' show
|
|
||||||
DatePickerEntryMode,
|
|
||||||
DatePickerMode,
|
|
||||||
DateTimeRange,
|
|
||||||
SelectableDayPredicate;
|
|
||||||
export 'date_picker_deprecated.dart';
|
|
||||||
export 'date_picker_dialog.dart' show showDatePicker;
|
|
||||||
export 'date_range_picker_dialog.dart' show showDateRangePicker;
|
|
||||||
export 'input_date_picker.dart' show InputDatePickerFormField;
|
|
||||||
|
|
||||||
// TODO(ianh): Not exporting everything is unusual and we should
|
|
||||||
// probably change to just exporting everything and making sure it's
|
|
||||||
// acceptable as a public API, or, worst case, merging the parts
|
|
||||||
// that really must be public into a single file and make them
|
|
||||||
// actually private.
|
|
835
packages/flutter/test/material/calendar_date_picker_test.dart
Normal file
835
packages/flutter/test/material/calendar_date_picker_test.dart
Normal file
|
@ -0,0 +1,835 @@
|
||||||
|
// 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/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../rendering/mock_canvas.dart';
|
||||||
|
import 'feedback_tester.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
final Finder nextMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Next month') ?? false));
|
||||||
|
final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false));
|
||||||
|
|
||||||
|
Widget calendarDatePicker({
|
||||||
|
Key? key,
|
||||||
|
DateTime? initialDate,
|
||||||
|
DateTime? firstDate,
|
||||||
|
DateTime? lastDate,
|
||||||
|
DateTime? currentDate,
|
||||||
|
ValueChanged<DateTime>? onDateChanged,
|
||||||
|
ValueChanged<DateTime>? onDisplayedMonthChanged,
|
||||||
|
DatePickerMode initialCalendarMode = DatePickerMode.day,
|
||||||
|
SelectableDayPredicate? selectableDayPredicate,
|
||||||
|
TextDirection textDirection = TextDirection.ltr,
|
||||||
|
}) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: textDirection,
|
||||||
|
child: CalendarDatePicker(
|
||||||
|
key: key,
|
||||||
|
initialDate: initialDate ?? DateTime(2016, DateTime.january, 15),
|
||||||
|
firstDate: firstDate ?? DateTime(2001, DateTime.january, 1),
|
||||||
|
lastDate: lastDate ?? DateTime(2031, DateTime.december, 31),
|
||||||
|
currentDate: currentDate ?? DateTime(2016, DateTime.january, 3),
|
||||||
|
onDateChanged: onDateChanged ?? (DateTime date) {},
|
||||||
|
onDisplayedMonthChanged: onDisplayedMonthChanged,
|
||||||
|
initialCalendarMode: initialCalendarMode,
|
||||||
|
selectableDayPredicate: selectableDayPredicate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('CalendarDatePicker', () {
|
||||||
|
testWidgets('Can select a day', (WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('12'));
|
||||||
|
expect(selectedDate, equals(DateTime(2016, DateTime.january, 12)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can select a month', (WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
expect(find.text('January 2016'), findsOneWidget);
|
||||||
|
|
||||||
|
// Go back two months
|
||||||
|
await tester.tap(previousMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('December 2015'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2015, DateTime.december, 1)));
|
||||||
|
await tester.tap(previousMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('November 2015'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2015, DateTime.november, 1)));
|
||||||
|
|
||||||
|
// Go forward a month
|
||||||
|
await tester.tap(nextMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('December 2015'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2015, DateTime.december, 1)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can select a year', (WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(find.text('January 2016')); // Switch to year mode.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('January 2018'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2018, DateTime.january, 1)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selecting date does not change displayed month', (WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialDate: DateTime(2020, DateTime.march, 15),
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(nextMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('April 2020'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2020, DateTime.april, 1)));
|
||||||
|
|
||||||
|
await tester.tap(find.text('25'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('April 2020'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2020, DateTime.april, 1)));
|
||||||
|
expect(selectedDate, equals(DateTime(2020, DateTime.april, 25)));
|
||||||
|
// There isn't a 31 in April so there shouldn't be one if it is showing April.
|
||||||
|
expect(find.text('31'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Changing year does not change selected date', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('4'));
|
||||||
|
expect(selectedDate, equals(DateTime(2016, DateTime.january, 4)));
|
||||||
|
await tester.tap(find.text('January 2016'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(selectedDate, equals(DateTime(2016, DateTime.january, 4)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Changing year does not change the month', (WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
await tester.tap(nextMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(nextMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('March 2016'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('March 2018'), findsOneWidget);
|
||||||
|
expect(displayedMonth, equals(DateTime(2018, DateTime.march, 1)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can select a year and then a day', (WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('January 2016')); // Switch to year mode.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2017'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('19'));
|
||||||
|
expect(selectedDate, equals(DateTime(2017, DateTime.january, 19)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Current year is visible in year picker', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
await tester.tap(find.text('January 2016')); // Switch to year mode.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('2016'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async {
|
||||||
|
final DateTime validDate = DateTime(2017, DateTime.january, 15);
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialDate: validDate,
|
||||||
|
firstDate: validDate,
|
||||||
|
lastDate: validDate,
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Earlier than firstDate. Should be ignored.
|
||||||
|
await tester.tap(find.text('10'));
|
||||||
|
expect(selectedDate, isNull);
|
||||||
|
|
||||||
|
// Later than lastDate. Should be ignored.
|
||||||
|
await tester.tap(find.text('20'));
|
||||||
|
expect(selectedDate, isNull);
|
||||||
|
|
||||||
|
// This one is just right.
|
||||||
|
await tester.tap(find.text('15'));
|
||||||
|
expect(selectedDate, validDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Cannot navigate to a month outside bounds', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
firstDate: DateTime(2016, DateTime.december, 15),
|
||||||
|
initialDate: DateTime(2017, DateTime.january, 15),
|
||||||
|
lastDate: DateTime(2017, DateTime.february, 15),
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.tap(nextMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(displayedMonth, equals(DateTime(2017, DateTime.february, 1)));
|
||||||
|
// Shouldn't be possible to keep going forward into March.
|
||||||
|
expect(nextMonthIcon, findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(previousMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(previousMonthIcon);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(displayedMonth, equals(DateTime(2016, DateTime.december, 1)));
|
||||||
|
// Shouldn't be possible to keep going backward into November.
|
||||||
|
expect(previousMonthIcon, findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Cannot select disabled year', (WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
firstDate: DateTime(2018, DateTime.june, 9),
|
||||||
|
initialDate: DateTime(2018, DateTime.july, 4),
|
||||||
|
lastDate: DateTime(2018, DateTime.december, 15),
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('July 2018')); // Switch to year mode.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2016')); // Disabled, doesn't change the year.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2020')); // Disabled, doesn't change the year.
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Nothing should have changed.
|
||||||
|
expect(displayedMonth, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selecting firstDate year respects firstDate', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
firstDate: DateTime(2016, DateTime.june, 9),
|
||||||
|
initialDate: DateTime(2018, DateTime.may, 4),
|
||||||
|
lastDate: DateTime(2019, DateTime.january, 15),
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('May 2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2016'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Month should be clamped to June as the range starts at June 2016.
|
||||||
|
expect(find.text('June 2016'), findsOneWidget);
|
||||||
|
expect(displayedMonth, DateTime(2016, DateTime.june, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selecting lastDate year respects lastDate', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? displayedMonth;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
firstDate: DateTime(2016, DateTime.june, 9),
|
||||||
|
initialDate: DateTime(2018, DateTime.may, 4),
|
||||||
|
lastDate: DateTime(2019, DateTime.january, 15),
|
||||||
|
onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('May 2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2019'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Month should be clamped to January as the range ends at January 2019.
|
||||||
|
expect(find.text('January 2019'), findsOneWidget);
|
||||||
|
expect(displayedMonth, DateTime(2019, DateTime.january, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Only predicate days are selectable', (WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
firstDate: DateTime(2017, DateTime.january, 10),
|
||||||
|
initialDate: DateTime(2017, DateTime.january, 16),
|
||||||
|
lastDate: DateTime(2017, DateTime.january, 20),
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
selectableDayPredicate: (DateTime date) => date.day.isEven,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('13')); // Odd, doesn't work.
|
||||||
|
expect(selectedDate, isNull);
|
||||||
|
await tester.tap(find.text('10')); // Even, works.
|
||||||
|
expect(selectedDate, DateTime(2017, DateTime.january, 10));
|
||||||
|
await tester.tap(find.text('17')); // Odd, doesn't work.
|
||||||
|
expect(selectedDate, DateTime(2017, DateTime.january, 10));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Can select initial calendar picker mode', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialDate: DateTime(2014, DateTime.january, 15),
|
||||||
|
initialCalendarMode: DatePickerMode.year,
|
||||||
|
));
|
||||||
|
// 2018 wouldn't be available if the year picker wasn't showing.
|
||||||
|
// The initial current year is 2014.
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('January 2018'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('currentDate is highlighted', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
currentDate: DateTime(2016, 1, 2),
|
||||||
|
));
|
||||||
|
const Color todayColor = Color(0xff2196f3); // default primary color
|
||||||
|
expect(
|
||||||
|
Material.of(tester.element(find.text('2'))),
|
||||||
|
// The current day should be painted with a circle outline.
|
||||||
|
paints
|
||||||
|
..circle(color: todayColor,
|
||||||
|
style: PaintingStyle.stroke,
|
||||||
|
strokeWidth: 1.0)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selecting date does not switch picker to year selection', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialDate: DateTime(2020, DateTime.may, 10),
|
||||||
|
initialCalendarMode: DatePickerMode.year,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('2017'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('May 2017'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('10'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('May 2017'), findsOneWidget);
|
||||||
|
expect(find.text('2017'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Updates to initialDate parameter is reflected in the state', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
final Key pickerKey = UniqueKey();
|
||||||
|
final DateTime initialDate = DateTime(2020, 1, 21);
|
||||||
|
final DateTime updatedDate = DateTime(1976, 2, 23);
|
||||||
|
final DateTime firstDate = DateTime(1970, 1, 1);
|
||||||
|
final DateTime lastDate = DateTime(2099, 31, 12);
|
||||||
|
const Color selectedColor = Color(0xff2196f3); // default primary color
|
||||||
|
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
key: pickerKey,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
onDateChanged: (DateTime value) {},
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Month should show as January 2020
|
||||||
|
expect(find.text('January 2020'), findsOneWidget);
|
||||||
|
// Selected date should be painted with a colored circle.
|
||||||
|
expect(
|
||||||
|
Material.of(tester.element(find.text('21'))),
|
||||||
|
paints..circle(color: selectedColor, style: PaintingStyle.fill)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change to the updated initialDate
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
key: pickerKey,
|
||||||
|
initialDate: updatedDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
onDateChanged: (DateTime value) {},
|
||||||
|
));
|
||||||
|
// Wait for the page scroll animation to finish.
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Month should show as February 1976
|
||||||
|
expect(find.text('January 2020'), findsNothing);
|
||||||
|
expect(find.text('February 1976'), findsOneWidget);
|
||||||
|
// Selected date should be painted with a colored circle.
|
||||||
|
expect(
|
||||||
|
Material.of(tester.element(find.text('23'))),
|
||||||
|
paints..circle(color: selectedColor, style: PaintingStyle.fill)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Updates to initialCalendarMode parameter is reflected in the state', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
final Key pickerKey = UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
key: pickerKey,
|
||||||
|
initialCalendarMode: DatePickerMode.year,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should be in year mode.
|
||||||
|
expect(find.text('January 2016'), findsOneWidget); // Day/year selector
|
||||||
|
expect(find.text('15'), findsNothing); // day 15 in grid
|
||||||
|
expect(find.text('2016'), findsOneWidget); // 2016 in year grid
|
||||||
|
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
key: pickerKey,
|
||||||
|
initialCalendarMode: DatePickerMode.day,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should be in day mode.
|
||||||
|
expect(find.text('January 2016'), findsOneWidget); // Day/year selector
|
||||||
|
expect(find.text('15'), findsOneWidget); // day 15 in grid
|
||||||
|
expect(find.text('2016'), findsNothing); // 2016 in year grid
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Keyboard navigation', () {
|
||||||
|
testWidgets('Can toggle to year mode', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
expect(find.text('2016'), findsNothing);
|
||||||
|
expect(find.text('January 2016'), findsOneWidget);
|
||||||
|
// Navigate to the year selector and activate it.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// The years should be visible.
|
||||||
|
expect(find.text('2016'), findsOneWidget);
|
||||||
|
expect(find.text('January 2016'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Can navigate next/previous months', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
expect(find.text('January 2016'), findsOneWidget);
|
||||||
|
// Navigate to the previous month button and activate it twice.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Should be showing Nov 2015
|
||||||
|
expect(find.text('November 2015'), findsOneWidget);
|
||||||
|
|
||||||
|
// Navigate to the next month button and activate it four times.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Should be on Mar 2016.
|
||||||
|
expect(find.text('March 2016'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can navigate date grid with arrow keys', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
// Navigate to the grid.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
|
||||||
|
// Navigate from Jan 15 to Jan 18 with arrow keys.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Activate it.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have selected Jan 18.
|
||||||
|
expect(selectedDate, DateTime(2016, DateTime.january, 18));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Navigating with arrow keys scrolls months', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
));
|
||||||
|
// Navigate to the grid.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate from Jan 15 to Dec 31 with arrow keys
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have scrolled to Dec 2015.
|
||||||
|
expect(find.text('December 2015'), findsOneWidget);
|
||||||
|
|
||||||
|
// Navigate from Dec 31 to Nov 26 with arrow keys.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have scrolled to Nov 2015.
|
||||||
|
expect(find.text('November 2015'), findsOneWidget);
|
||||||
|
|
||||||
|
// Activate it
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have selected Jan 18.
|
||||||
|
expect(selectedDate, DateTime(2015, DateTime.november, 26));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'RTL text direction reverses the horizontal arrow key navigation', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
DateTime? selectedDate;
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
onDateChanged: (DateTime date) => selectedDate = date,
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
));
|
||||||
|
// Navigate to the grid.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate from Jan 15 to 19 with arrow keys.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Activate it.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Navigate out of the grid and to the OK button.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Activate OK.
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have selected Jan 18.
|
||||||
|
expect(selectedDate, DateTime(2016, DateTime.january, 19));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Haptic feedback', () {
|
||||||
|
const Duration hapticFeedbackInterval = Duration(milliseconds: 10);
|
||||||
|
late FeedbackTester feedback;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
feedback = FeedbackTester();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
feedback.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Selecting date vibrates', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
await tester.tap(find.text('10'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 1);
|
||||||
|
await tester.tap(find.text('12'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 2);
|
||||||
|
await tester.tap(find.text('14'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Tapping unselectable date does not vibrate', (
|
||||||
|
WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialDate: DateTime(2016, DateTime.january, 10),
|
||||||
|
selectableDayPredicate: (DateTime date) => date.day.isEven,
|
||||||
|
));
|
||||||
|
await tester.tap(find.text('11'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 0);
|
||||||
|
await tester.tap(find.text('13'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 0);
|
||||||
|
await tester.tap(find.text('15'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Changing modes and year vibrates', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
await tester.tap(find.text('January 2016'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 1);
|
||||||
|
await tester.tap(find.text('2018'));
|
||||||
|
await tester.pump(hapticFeedbackInterval);
|
||||||
|
expect(feedback.hapticCount, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Semantics', () {
|
||||||
|
testWidgets('day mode', (WidgetTester tester) async {
|
||||||
|
final SemanticsHandle semantics = tester.ensureSemantics();
|
||||||
|
addTearDown(semantics.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(calendarDatePicker());
|
||||||
|
|
||||||
|
// Year mode drop down button.
|
||||||
|
expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
|
||||||
|
label: 'Select year',
|
||||||
|
isButton: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Prev/Next month buttons.
|
||||||
|
expect(tester.getSemantics(previousMonthIcon), matchesSemantics(
|
||||||
|
label: 'Previous month December 2015',
|
||||||
|
isButton: true,
|
||||||
|
hasTapAction: true,
|
||||||
|
isEnabled: true,
|
||||||
|
hasEnabledState: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(nextMonthIcon), matchesSemantics(
|
||||||
|
label: 'Next month February 2016',
|
||||||
|
isButton: true,
|
||||||
|
hasTapAction: true,
|
||||||
|
isEnabled: true,
|
||||||
|
hasEnabledState: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Day grid.
|
||||||
|
expect(tester.getSemantics(find.text('1')), matchesSemantics(
|
||||||
|
label: '1, Friday, January 1, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('2')), matchesSemantics(
|
||||||
|
label: '2, Saturday, January 2, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('3')), matchesSemantics(
|
||||||
|
label: '3, Sunday, January 3, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('4')), matchesSemantics(
|
||||||
|
label: '4, Monday, January 4, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('5')), matchesSemantics(
|
||||||
|
label: '5, Tuesday, January 5, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('6')), matchesSemantics(
|
||||||
|
label: '6, Wednesday, January 6, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('7')), matchesSemantics(
|
||||||
|
label: '7, Thursday, January 7, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('8')), matchesSemantics(
|
||||||
|
label: '8, Friday, January 8, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('9')), matchesSemantics(
|
||||||
|
label: '9, Saturday, January 9, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('10')), matchesSemantics(
|
||||||
|
label: '10, Sunday, January 10, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('11')), matchesSemantics(
|
||||||
|
label: '11, Monday, January 11, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('12')), matchesSemantics(
|
||||||
|
label: '12, Tuesday, January 12, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('13')), matchesSemantics(
|
||||||
|
label: '13, Wednesday, January 13, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('14')), matchesSemantics(
|
||||||
|
label: '14, Thursday, January 14, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('15')), matchesSemantics(
|
||||||
|
label: '15, Friday, January 15, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isSelected: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('16')), matchesSemantics(
|
||||||
|
label: '16, Saturday, January 16, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('17')), matchesSemantics(
|
||||||
|
label: '17, Sunday, January 17, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('18')), matchesSemantics(
|
||||||
|
label: '18, Monday, January 18, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('19')), matchesSemantics(
|
||||||
|
label: '19, Tuesday, January 19, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('20')), matchesSemantics(
|
||||||
|
label: '20, Wednesday, January 20, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('21')), matchesSemantics(
|
||||||
|
label: '21, Thursday, January 21, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('22')), matchesSemantics(
|
||||||
|
label: '22, Friday, January 22, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('23')), matchesSemantics(
|
||||||
|
label: '23, Saturday, January 23, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('24')), matchesSemantics(
|
||||||
|
label: '24, Sunday, January 24, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('25')), matchesSemantics(
|
||||||
|
label: '25, Monday, January 25, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('26')), matchesSemantics(
|
||||||
|
label: '26, Tuesday, January 26, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('27')), matchesSemantics(
|
||||||
|
label: '27, Wednesday, January 27, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('28')), matchesSemantics(
|
||||||
|
label: '28, Thursday, January 28, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('29')), matchesSemantics(
|
||||||
|
label: '29, Friday, January 29, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
expect(tester.getSemantics(find.text('30')), matchesSemantics(
|
||||||
|
label: '30, Saturday, January 30, 2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('calendar year mode', (WidgetTester tester) async {
|
||||||
|
final SemanticsHandle semantics = tester.ensureSemantics();
|
||||||
|
addTearDown(semantics.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(calendarDatePicker(
|
||||||
|
initialCalendarMode: DatePickerMode.year,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Year mode drop down button.
|
||||||
|
expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
|
||||||
|
label: 'Select year',
|
||||||
|
isButton: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Year grid only shows 2010 - 2024.
|
||||||
|
for (int year = 2010; year <= 2024; year++) {
|
||||||
|
expect(tester.getSemantics(find.text('$year')), matchesSemantics(
|
||||||
|
label: '$year',
|
||||||
|
hasTapAction: true,
|
||||||
|
isSelected: year == 2016,
|
||||||
|
isFocusable: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -7,27 +7,9 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
import '../rendering/mock_canvas.dart';
|
||||||
import 'feedback_tester.dart';
|
|
||||||
|
|
||||||
class MockClipboard {
|
|
||||||
dynamic _clipboardData = <String, dynamic>{
|
|
||||||
'text': null,
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
|
||||||
switch (methodCall.method) {
|
|
||||||
case 'Clipboard.getData':
|
|
||||||
return _clipboardData;
|
|
||||||
case 'Clipboard.setData':
|
|
||||||
_clipboardData = methodCall.arguments;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final MockClipboard mockClipboard = MockClipboard();
|
|
||||||
|
|
||||||
late DateTime firstDate;
|
late DateTime firstDate;
|
||||||
late DateTime lastDate;
|
late DateTime lastDate;
|
||||||
|
@ -54,7 +36,7 @@ void main() {
|
||||||
return tester.widget<TextField>(find.byType(TextField));
|
return tester.widget<TextField>(find.byType(TextField));
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() {
|
||||||
firstDate = DateTime(2001, DateTime.january, 1);
|
firstDate = DateTime(2001, DateTime.january, 1);
|
||||||
lastDate = DateTime(2031, DateTime.december, 31);
|
lastDate = DateTime(2031, DateTime.december, 31);
|
||||||
initialDate = DateTime(2016, DateTime.january, 15);
|
initialDate = DateTime(2016, DateTime.january, 15);
|
||||||
|
@ -70,15 +52,6 @@ void main() {
|
||||||
fieldHintText = null;
|
fieldHintText = null;
|
||||||
fieldLabelText = null;
|
fieldLabelText = null;
|
||||||
helpText = null;
|
helpText = null;
|
||||||
|
|
||||||
// Fill the clipboard so that the Paste option is available in the text
|
|
||||||
// selection menu.
|
|
||||||
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
|
||||||
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() {
|
|
||||||
SystemChannels.platform.setMockMethodCallHandler(null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> prepareDatePicker(
|
Future<void> prepareDatePicker(
|
||||||
|
@ -779,180 +752,10 @@ void main() {
|
||||||
expect(find.text(errorInvalidText!), findsOneWidget);
|
expect(find.text(errorInvalidText!), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {
|
|
||||||
late BuildContext buttonContext;
|
|
||||||
const InputBorder border = InputBorder.none;
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
|
||||||
theme: ThemeData.light().copyWith(
|
|
||||||
inputDecorationTheme: const InputDecorationTheme(
|
|
||||||
filled: false,
|
|
||||||
border: border,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
home: Material(
|
|
||||||
child: Builder(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
buttonContext = context;
|
|
||||||
},
|
|
||||||
child: const Text('Go'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
await tester.tap(find.text('Go'));
|
|
||||||
expect(buttonContext, isNotNull);
|
|
||||||
|
|
||||||
showDatePicker(
|
|
||||||
context: buttonContext,
|
|
||||||
initialDate: initialDate,
|
|
||||||
firstDate: firstDate,
|
|
||||||
lastDate: lastDate,
|
|
||||||
currentDate: today,
|
|
||||||
initialEntryMode: DatePickerEntryMode.input,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Get the border and container color from the painter of the _BorderContainer
|
|
||||||
// (this was cribbed from input_decorator_test.dart).
|
|
||||||
final CustomPaint customPaint = tester.widget(find.descendant(
|
|
||||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
|
|
||||||
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
|
|
||||||
));
|
|
||||||
final dynamic/*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter;
|
|
||||||
final dynamic/*_InputBorderTween*/ inputBorderTween = inputBorderPainter.border;
|
|
||||||
final Animation<double> animation = inputBorderPainter.borderAnimation as Animation<double>;
|
|
||||||
final InputBorder actualBorder = inputBorderTween.evaluate(animation) as InputBorder;
|
|
||||||
final Color containerColor = inputBorderPainter.blendedColor as Color;
|
|
||||||
|
|
||||||
// Border should match
|
|
||||||
expect(actualBorder, equals(border));
|
|
||||||
|
|
||||||
// It shouldn't be filled, so the color should be transparent
|
|
||||||
expect(containerColor, equals(Colors.transparent));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('CalendarDatePicker', () {
|
|
||||||
// Tests for the standalone CalendarDatePicker class
|
|
||||||
testWidgets('Updates to initialDate parameter is reflected in the state', (WidgetTester tester) async {
|
|
||||||
final Key pickerKey = UniqueKey();
|
|
||||||
final DateTime initialDate = DateTime(2020, 1, 21);
|
|
||||||
final DateTime updatedDate = DateTime(1976, 2, 23);
|
|
||||||
const Color selectedColor = Color(0xff2196f3); // default primary color
|
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: CalendarDatePicker(
|
|
||||||
key: pickerKey,
|
|
||||||
initialDate: initialDate,
|
|
||||||
firstDate: DateTime(1970, 1, 1),
|
|
||||||
lastDate: DateTime(2099, 31, 12),
|
|
||||||
onDateChanged: (DateTime value) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
// Month should show as January 2020
|
|
||||||
expect(find.text('January 2020'), findsOneWidget);
|
|
||||||
// Selected date should be painted with a colored circle
|
|
||||||
expect(
|
|
||||||
Material.of(tester.element(find.text('21'))),
|
|
||||||
paints..circle(color: selectedColor, style: PaintingStyle.fill)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Change to the updated initialDate
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: CalendarDatePicker(
|
|
||||||
key: pickerKey,
|
|
||||||
initialDate: updatedDate,
|
|
||||||
firstDate: DateTime(1970, 1, 1),
|
|
||||||
lastDate: DateTime(2099, 31, 12),
|
|
||||||
onDateChanged: (DateTime value) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
// Wait for the page scroll animation to finish
|
|
||||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
|
||||||
|
|
||||||
// Month should show as February 1976
|
|
||||||
expect(find.text('January 2020'), findsNothing);
|
|
||||||
expect(find.text('February 1976'), findsOneWidget);
|
|
||||||
// Selected date should be painted with a colored circle
|
|
||||||
expect(
|
|
||||||
Material.of(tester.element(find.text('23'))),
|
|
||||||
paints..circle(color: selectedColor, style: PaintingStyle.fill)
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Haptic feedback', () {
|
|
||||||
const Duration hapticFeedbackInterval = Duration(milliseconds: 10);
|
|
||||||
late FeedbackTester feedback;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
feedback = FeedbackTester();
|
|
||||||
initialDate = DateTime(2017, DateTime.january, 16);
|
|
||||||
firstDate = DateTime(2017, DateTime.january, 10);
|
|
||||||
lastDate = DateTime(2018, DateTime.january, 20);
|
|
||||||
initialCalendarMode = DatePickerMode.day;
|
|
||||||
selectableDayPredicate = (DateTime date) => date.day.isEven;
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() {
|
|
||||||
feedback.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Selecting date vibrates', (WidgetTester tester) async {
|
|
||||||
await prepareDatePicker(tester, (Future<DateTime?> date) async {
|
|
||||||
await tester.tap(find.text('10'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 1);
|
|
||||||
await tester.tap(find.text('12'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 2);
|
|
||||||
await tester.tap(find.text('14'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
|
|
||||||
await prepareDatePicker(tester, (Future<DateTime?> date) async {
|
|
||||||
await tester.tap(find.text('11'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 0);
|
|
||||||
await tester.tap(find.text('13'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 0);
|
|
||||||
await tester.tap(find.text('15'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Changing modes and year vibrates', (WidgetTester tester) async {
|
|
||||||
await prepareDatePicker(tester, (Future<DateTime?> date) async {
|
|
||||||
await tester.tap(find.text('January 2017'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 1);
|
|
||||||
await tester.tap(find.text('2018'));
|
|
||||||
await tester.pump(hapticFeedbackInterval);
|
|
||||||
expect(feedback.hapticCount, 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Semantics', () {
|
group('Semantics', () {
|
||||||
testWidgets('calendar day mode', (WidgetTester tester) async {
|
testWidgets('calendar mode', (WidgetTester tester) async {
|
||||||
final SemanticsHandle semantics = tester.ensureSemantics();
|
final SemanticsHandle semantics = tester.ensureSemantics();
|
||||||
addTearDown(semantics.dispose);
|
addTearDown(semantics.dispose);
|
||||||
|
|
||||||
|
@ -972,239 +775,7 @@ void main() {
|
||||||
isFocusable: true,
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Year mode drop down button
|
// The semantics of the CalendarDatePicker are tested in its tests.
|
||||||
expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
|
|
||||||
label: 'Select year',
|
|
||||||
isButton: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Prev/Next month buttons
|
|
||||||
expect(tester.getSemantics(previousMonthIcon), matchesSemantics(
|
|
||||||
label: 'Previous month December 2015',
|
|
||||||
isButton: true,
|
|
||||||
hasTapAction: true,
|
|
||||||
isEnabled: true,
|
|
||||||
hasEnabledState: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(nextMonthIcon), matchesSemantics(
|
|
||||||
label: 'Next month February 2016',
|
|
||||||
isButton: true,
|
|
||||||
hasTapAction: true,
|
|
||||||
isEnabled: true,
|
|
||||||
hasEnabledState: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Day grid
|
|
||||||
expect(tester.getSemantics(find.text('1')), matchesSemantics(
|
|
||||||
label: '1, Friday, January 1, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('2')), matchesSemantics(
|
|
||||||
label: '2, Saturday, January 2, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('3')), matchesSemantics(
|
|
||||||
label: '3, Sunday, January 3, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('4')), matchesSemantics(
|
|
||||||
label: '4, Monday, January 4, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('5')), matchesSemantics(
|
|
||||||
label: '5, Tuesday, January 5, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('6')), matchesSemantics(
|
|
||||||
label: '6, Wednesday, January 6, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('7')), matchesSemantics(
|
|
||||||
label: '7, Thursday, January 7, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('8')), matchesSemantics(
|
|
||||||
label: '8, Friday, January 8, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('9')), matchesSemantics(
|
|
||||||
label: '9, Saturday, January 9, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('10')), matchesSemantics(
|
|
||||||
label: '10, Sunday, January 10, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('11')), matchesSemantics(
|
|
||||||
label: '11, Monday, January 11, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('12')), matchesSemantics(
|
|
||||||
label: '12, Tuesday, January 12, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('13')), matchesSemantics(
|
|
||||||
label: '13, Wednesday, January 13, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('14')), matchesSemantics(
|
|
||||||
label: '14, Thursday, January 14, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('15')), matchesSemantics(
|
|
||||||
label: '15, Friday, January 15, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isSelected: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('16')), matchesSemantics(
|
|
||||||
label: '16, Saturday, January 16, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('17')), matchesSemantics(
|
|
||||||
label: '17, Sunday, January 17, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('18')), matchesSemantics(
|
|
||||||
label: '18, Monday, January 18, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('19')), matchesSemantics(
|
|
||||||
label: '19, Tuesday, January 19, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('20')), matchesSemantics(
|
|
||||||
label: '20, Wednesday, January 20, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('21')), matchesSemantics(
|
|
||||||
label: '21, Thursday, January 21, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('22')), matchesSemantics(
|
|
||||||
label: '22, Friday, January 22, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('23')), matchesSemantics(
|
|
||||||
label: '23, Saturday, January 23, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('24')), matchesSemantics(
|
|
||||||
label: '24, Sunday, January 24, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('25')), matchesSemantics(
|
|
||||||
label: '25, Monday, January 25, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('26')), matchesSemantics(
|
|
||||||
label: '26, Tuesday, January 26, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('27')), matchesSemantics(
|
|
||||||
label: '27, Wednesday, January 27, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('28')), matchesSemantics(
|
|
||||||
label: '28, Thursday, January 28, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('29')), matchesSemantics(
|
|
||||||
label: '29, Friday, January 29, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('30')), matchesSemantics(
|
|
||||||
label: '30, Saturday, January 30, 2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Ok/Cancel buttons
|
|
||||||
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
|
|
||||||
label: 'OK',
|
|
||||||
isButton: true,
|
|
||||||
hasTapAction: true,
|
|
||||||
isEnabled: true,
|
|
||||||
hasEnabledState: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
expect(tester.getSemantics(find.text('CANCEL')), matchesSemantics(
|
|
||||||
label: 'CANCEL',
|
|
||||||
isButton: true,
|
|
||||||
hasTapAction: true,
|
|
||||||
isEnabled: true,
|
|
||||||
hasEnabledState: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('calendar year mode', (WidgetTester tester) async {
|
|
||||||
final SemanticsHandle semantics = tester.ensureSemantics();
|
|
||||||
addTearDown(semantics.dispose);
|
|
||||||
|
|
||||||
initialCalendarMode = DatePickerMode.year;
|
|
||||||
await prepareDatePicker(tester, (Future<DateTime?> date) async {
|
|
||||||
// Header
|
|
||||||
expect(tester.getSemantics(find.text('SELECT DATE')), matchesSemantics(
|
|
||||||
label: 'SELECT DATE\nFri, Jan 15',
|
|
||||||
));
|
|
||||||
|
|
||||||
// Input mode toggle button
|
|
||||||
expect(tester.getSemantics(switchToInputIcon), matchesSemantics(
|
|
||||||
label: 'Switch to input',
|
|
||||||
isButton: true,
|
|
||||||
hasTapAction: true,
|
|
||||||
isEnabled: true,
|
|
||||||
hasEnabledState: true,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Year mode drop down button
|
|
||||||
expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
|
|
||||||
label: 'Select year',
|
|
||||||
isButton: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Year grid only shows 2010 - 2024
|
|
||||||
for (int year = 2010; year <= 2024; year++) {
|
|
||||||
expect(tester.getSemantics(find.text('$year')), matchesSemantics(
|
|
||||||
label: '$year',
|
|
||||||
hasTapAction: true,
|
|
||||||
isSelected: year == 2016,
|
|
||||||
isFocusable: true,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok/Cancel buttons
|
// Ok/Cancel buttons
|
||||||
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
|
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
|
||||||
|
@ -1247,20 +818,7 @@ void main() {
|
||||||
isFocusable: true,
|
isFocusable: true,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Text field
|
// The semantics of the InputDatePickerFormField are tested in its tests.
|
||||||
expect(tester.getSemantics(find.byType(EditableText)), matchesSemantics(
|
|
||||||
label: 'Enter Date\nmm/dd/yyyy',
|
|
||||||
isTextField: true,
|
|
||||||
isFocused: true,
|
|
||||||
value: '01/15/2016',
|
|
||||||
hasTapAction: true,
|
|
||||||
hasSetSelectionAction: true,
|
|
||||||
hasCopyAction: true,
|
|
||||||
hasCutAction: true,
|
|
||||||
hasPasteAction: true,
|
|
||||||
hasMoveCursorBackwardByCharacterAction: true,
|
|
||||||
hasMoveCursorBackwardByWordAction: true,
|
|
||||||
));
|
|
||||||
|
|
||||||
// Ok/Cancel buttons
|
// Ok/Cancel buttons
|
||||||
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
|
expect(tester.getSemantics(find.text('OK')), matchesSemantics(
|
||||||
|
|
|
@ -0,0 +1,314 @@
|
||||||
|
// 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/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final MockClipboard mockClipboard = MockClipboard();
|
||||||
|
|
||||||
|
Widget _inputDatePickerField({
|
||||||
|
Key? key,
|
||||||
|
DateTime? initialDate,
|
||||||
|
DateTime? firstDate,
|
||||||
|
DateTime? lastDate,
|
||||||
|
ValueChanged<DateTime>? onDateSubmitted,
|
||||||
|
ValueChanged<DateTime>? onDateSaved,
|
||||||
|
SelectableDayPredicate? selectableDayPredicate,
|
||||||
|
String? errorFormatText,
|
||||||
|
String? errorInvalidText,
|
||||||
|
String? fieldHintText,
|
||||||
|
String? fieldLabelText,
|
||||||
|
bool autofocus = false,
|
||||||
|
Key? formKey,
|
||||||
|
ThemeData? theme,
|
||||||
|
}) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: theme ?? ThemeData.from(colorScheme: const ColorScheme.light()),
|
||||||
|
home: Material(
|
||||||
|
child: Form(
|
||||||
|
key: formKey,
|
||||||
|
child: InputDatePickerFormField(
|
||||||
|
key: key,
|
||||||
|
initialDate: initialDate ?? DateTime(2016, DateTime.january, 15),
|
||||||
|
firstDate: firstDate ?? DateTime(2001, DateTime.january, 1),
|
||||||
|
lastDate: lastDate ?? DateTime(2031, DateTime.december, 31),
|
||||||
|
onDateSubmitted: onDateSubmitted,
|
||||||
|
onDateSaved: onDateSaved,
|
||||||
|
selectableDayPredicate: selectableDayPredicate,
|
||||||
|
errorFormatText: errorFormatText,
|
||||||
|
errorInvalidText: errorInvalidText,
|
||||||
|
fieldHintText: fieldHintText,
|
||||||
|
fieldLabelText: fieldLabelText,
|
||||||
|
autofocus: autofocus,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField _textField(WidgetTester tester) {
|
||||||
|
return tester.widget<TextField>(find.byType(TextField));
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEditingController _textFieldController(WidgetTester tester) {
|
||||||
|
return _textField(tester).controller!;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _textOpacity(WidgetTester tester, String textValue) {
|
||||||
|
final FadeTransition opacityWidget = tester.widget<FadeTransition>(
|
||||||
|
find.ancestor(
|
||||||
|
of: find.text(textValue),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
).first,
|
||||||
|
);
|
||||||
|
return opacityWidget.opacity.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
group('InputDatePickerFormField', () {
|
||||||
|
|
||||||
|
testWidgets('Initial date is the default', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
final DateTime initialDate = DateTime(2016, DateTime.february, 21);
|
||||||
|
DateTime? inputDate;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
initialDate: initialDate,
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
expect(_textFieldController(tester).value.text, equals('02/21/2016'));
|
||||||
|
formKey.currentState!.save();
|
||||||
|
expect(inputDate, equals(initialDate));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Changing initial date is reflected in text value', (WidgetTester tester) async {
|
||||||
|
final DateTime initialDate = DateTime(2016, DateTime.february, 21);
|
||||||
|
final DateTime updatedInitialDate = DateTime(2016, DateTime.february, 23);
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
initialDate: initialDate,
|
||||||
|
));
|
||||||
|
expect(_textFieldController(tester).value.text, equals('02/21/2016'));
|
||||||
|
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
initialDate: updatedInitialDate,
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(_textFieldController(tester).value.text, equals('02/23/2016'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Valid date entry', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
DateTime? inputDate;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
|
||||||
|
_textFieldController(tester).text = '02/21/2016';
|
||||||
|
formKey.currentState!.save();
|
||||||
|
expect(inputDate, equals(DateTime(2016, DateTime.february, 21)));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Invalid text entry shows errorFormat text', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
DateTime? inputDate;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
// Default errorFormat text
|
||||||
|
expect(find.text('Invalid format.'), findsNothing);
|
||||||
|
await tester.enterText(find.byType(TextField), 'foobar');
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inputDate, isNull);
|
||||||
|
expect(find.text('Invalid format.'), findsOneWidget);
|
||||||
|
|
||||||
|
// Change to a custom errorFormat text
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
errorFormatText: 'That is not a date.',
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Invalid format.'), findsNothing);
|
||||||
|
expect(find.text('That is not a date.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Valid text entry, but date outside first or last date shows bounds shows errorInvalid text', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
DateTime? inputDate;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
firstDate: DateTime(1966, DateTime.february, 21),
|
||||||
|
lastDate: DateTime(2040, DateTime.february, 23),
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
// Default errorInvalid text
|
||||||
|
expect(find.text('Out of range.'), findsNothing);
|
||||||
|
// Before first date
|
||||||
|
await tester.enterText(find.byType(TextField), '02/21/1950');
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inputDate, isNull);
|
||||||
|
expect(find.text('Out of range.'), findsOneWidget);
|
||||||
|
// After last date
|
||||||
|
await tester.enterText(find.byType(TextField), '02/23/2050');
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inputDate, isNull);
|
||||||
|
expect(find.text('Out of range.'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
errorInvalidText: 'Not in given range.',
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Out of range.'), findsNothing);
|
||||||
|
expect(find.text('Not in given range.'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('selectableDatePredicate will be used to show errorInvalid if date is not selectable', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
|
DateTime? inputDate;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
initialDate: DateTime(2016, DateTime.january, 16),
|
||||||
|
onDateSaved: (DateTime date) => inputDate = date,
|
||||||
|
selectableDayPredicate: (DateTime date) => date.day.isEven,
|
||||||
|
formKey: formKey,
|
||||||
|
));
|
||||||
|
// Default errorInvalid text
|
||||||
|
expect(find.text('Out of range.'), findsNothing);
|
||||||
|
// Odd day shouldn't be valid
|
||||||
|
await tester.enterText(find.byType(TextField), '02/21/1966');
|
||||||
|
expect(formKey.currentState!.validate(), isFalse);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inputDate, isNull);
|
||||||
|
expect(find.text('Out of range.'), findsOneWidget);
|
||||||
|
// Even day is valid
|
||||||
|
await tester.enterText(find.byType(TextField), '02/24/2030');
|
||||||
|
expect(formKey.currentState!.validate(), isTrue);
|
||||||
|
formKey.currentState!.save();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(inputDate, equals(DateTime(2030, DateTime.february, 24)));
|
||||||
|
expect(find.text('Out of range.'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Empty field shows hint text when focused', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_inputDatePickerField());
|
||||||
|
// Focus on it
|
||||||
|
await tester.tap(find.byType(TextField));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Hint text should be invisible
|
||||||
|
expect(_textOpacity(tester, 'mm/dd/yyyy'), equals(0.0));
|
||||||
|
_textFieldController(tester).clear();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
// Hint text should be visible
|
||||||
|
expect(_textOpacity(tester, 'mm/dd/yyyy'), equals(1.0));
|
||||||
|
|
||||||
|
// Change to a different hint text
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(fieldHintText: 'Enter some date'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('mm/dd/yyyy'), findsNothing);
|
||||||
|
expect(_textOpacity(tester, 'Enter some date'), equals(1.0));
|
||||||
|
await tester.enterText(find.byType(TextField), 'foobar');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(_textOpacity(tester, 'Enter some date'), equals(0.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Label text', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_inputDatePickerField());
|
||||||
|
// Default label
|
||||||
|
expect(find.text('Enter Date'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
fieldLabelText: 'Give me a date!',
|
||||||
|
));
|
||||||
|
expect(find.text('Enter Date'), findsNothing);
|
||||||
|
expect(find.text('Give me a date!'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsHandle semantics = tester.ensureSemantics();
|
||||||
|
addTearDown(semantics.dispose);
|
||||||
|
|
||||||
|
// Fill the clipboard so that the Paste option is available in the text
|
||||||
|
// selection menu.
|
||||||
|
SystemChannels.platform.setMockMethodCallHandler(mockClipboard.handleMethodCall);
|
||||||
|
await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
|
||||||
|
addTearDown(() => SystemChannels.platform.setMockMethodCallHandler(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(autofocus: true));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.getSemantics(find.byType(EditableText)), matchesSemantics(
|
||||||
|
label: 'Enter Date\nmm/dd/yyyy',
|
||||||
|
isTextField: true,
|
||||||
|
isFocused: true,
|
||||||
|
value: '01/15/2016',
|
||||||
|
hasTapAction: true,
|
||||||
|
hasSetSelectionAction: true,
|
||||||
|
hasCopyAction: true,
|
||||||
|
hasCutAction: true,
|
||||||
|
hasPasteAction: true,
|
||||||
|
hasMoveCursorBackwardByCharacterAction: true,
|
||||||
|
hasMoveCursorBackwardByWordAction: true,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {
|
||||||
|
const InputBorder border = InputBorder.none;
|
||||||
|
await tester.pumpWidget(_inputDatePickerField(
|
||||||
|
theme: ThemeData.from(colorScheme: const ColorScheme.light()).copyWith(
|
||||||
|
inputDecorationTheme: const InputDecorationTheme(
|
||||||
|
filled: false,
|
||||||
|
border: border,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Get the border and container color from the painter of the _BorderContainer
|
||||||
|
// (this was cribbed from input_decorator_test.dart).
|
||||||
|
final CustomPaint customPaint = tester.widget(find.descendant(
|
||||||
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
|
||||||
|
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
|
||||||
|
));
|
||||||
|
final dynamic/*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter;
|
||||||
|
final dynamic/*_InputBorderTween*/ inputBorderTween = inputBorderPainter.border;
|
||||||
|
final Animation<double> animation = inputBorderPainter.borderAnimation as Animation<double>;
|
||||||
|
final InputBorder actualBorder = inputBorderTween.evaluate(animation) as InputBorder;
|
||||||
|
final Color containerColor = inputBorderPainter.blendedColor as Color;
|
||||||
|
|
||||||
|
// Border should match
|
||||||
|
expect(actualBorder, equals(border));
|
||||||
|
|
||||||
|
// It shouldn't be filled, so the color should be transparent
|
||||||
|
expect(containerColor, equals(Colors.transparent));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockClipboard {
|
||||||
|
dynamic _clipboardData = <String, dynamic>{
|
||||||
|
'text': null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<dynamic> handleMethodCall(MethodCall methodCall) async {
|
||||||
|
switch (methodCall.method) {
|
||||||
|
case 'Clipboard.getData':
|
||||||
|
return _clipboardData;
|
||||||
|
case 'Clipboard.setData':
|
||||||
|
_clipboardData = methodCall.arguments;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue