Material Date Picker redesign (#50546)

Date Picker UI redesign
This commit is contained in:
Darren Austin 2020-03-24 18:44:57 -07:00 committed by GitHub
parent 25ef78e234
commit 142b526f1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 3999 additions and 1450 deletions

View file

@ -41,17 +41,23 @@ ConstructorGenerator generateMaterialConstructor = (LocaleInfo locale) {
const MaterialLocalization${locale.camelCase()}({
String localeName = '$localeName',
@required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : super(
localeName: localeName,
fullYearFormat: fullYearFormat,
compactDateFormat: compactDateFormat,
shortDateFormat: shortDateFormat,
mediumDateFormat: mediumDateFormat,
longDateFormat: longDateFormat,
yearMonthFormat: yearMonthFormat,
shortMonthDayFormat: shortMonthDayFormat,
decimalFormat: decimalFormat,
twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat,
);''';
@ -63,15 +69,18 @@ const String materialFactoryDeclaration = '''
GlobalMaterialLocalizations getMaterialTranslation(
Locale locale,
intl.DateFormat fullYearFormat,
intl.DateFormat compactDateFormat,
intl.DateFormat shortDateFormat,
intl.DateFormat mediumDateFormat,
intl.DateFormat longDateFormat,
intl.DateFormat yearMonthFormat,
intl.DateFormat shortMonthDayFormat,
intl.NumberFormat decimalFormat,
intl.NumberFormat twoDigitZeroPaddedFormat,
) {''';
const String materialFactoryArguments =
'fullYearFormat: fullYearFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
'fullYearFormat: fullYearFormat, compactDateFormat: compactDateFormat, shortDateFormat: shortDateFormat, mediumDateFormat: mediumDateFormat, longDateFormat: longDateFormat, yearMonthFormat: yearMonthFormat, shortMonthDayFormat: shortMonthDayFormat, decimalFormat: decimalFormat, twoDigitZeroPaddedFormat: twoDigitZeroPaddedFormat';
const String materialSupportedLanguagesConstant = 'kMaterialSupportedLanguages';

View file

@ -46,7 +46,6 @@ export 'src/material/colors.dart';
export 'src/material/constants.dart';
export 'src/material/data_table.dart';
export 'src/material/data_table_source.dart';
export 'src/material/date_picker.dart';
export 'src/material/debug.dart';
export 'src/material/dialog.dart';
export 'src/material/dialog_theme.dart';
@ -86,6 +85,7 @@ export 'src/material/outline_button.dart';
export 'src/material/page.dart';
export 'src/material/page_transitions_theme.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_theme.dart';
export 'src/material/progress_indicator.dart';

View file

@ -224,6 +224,29 @@ abstract class MaterialLocalizations {
/// Full unabbreviated year format, e.g. 2017 rather than 17.
String formatYear(DateTime date);
/// Formats the date in a compact format.
///
/// Usually just the numeric values for the for day, month and year are used.
///
/// Examples:
///
/// - US English: 02/21/2019
/// - Russian: 21.02.2019
///
/// See also:
/// * [parseCompactDate], which will convert a compact date string to a [DateTime].
String formatCompactDate(DateTime date);
/// Formats the date using a short-width format.
///
/// Includes the abbreviation of the month, the day and year.
///
/// Examples:
///
/// - US English: Feb 21, 2019
/// - Russian: 21 февр. 2019 г.
String formatShortDate(DateTime date);
/// Formats the date using a medium-width format.
///
/// Abbreviates month and days of week. This appears in the header of the date
@ -252,6 +275,24 @@ abstract class MaterialLocalizations {
/// in the date picker invoked using [showDatePicker].
String formatMonthYear(DateTime date);
/// Formats the month and day of the given [date].
///
/// Examples:
///
/// - US English: Feb 21
/// - Russian: 21 февр.
String formatShortMonthDay(DateTime date);
/// Converts the given compact date formatted string into a [DateTime].
///
/// The format of the string must be a valid compact date format for the
/// given locale. If the text doesn't represent a valid date, `null` will be
/// returned.
///
/// See also:
/// * [formatCompactDate], which will convert a [DateTime] into a string in the compact format.
DateTime parseCompactDate(String inputString);
/// List of week day names in narrow format, usually 1- or 2-letter
/// abbreviations of full names.
///
@ -437,6 +478,23 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
'December',
];
/// 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];
}
@override
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }) {
final TimeOfDayFormat format = timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat);
@ -470,6 +528,21 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override
String formatYear(DateTime date) => date.year.toString();
@override
String formatCompactDate(DateTime date) {
// Assumes US mm/dd/yyyy format
final String month = _formatTwoDigitZeroPad(date.month);
final String day = _formatTwoDigitZeroPad(date.day);
final String year = date.year.toString().padLeft(4, '0');
return '$month/$day/$year';
}
@override
String formatShortDate(DateTime date) {
final String month = _shortMonths[date.month - DateTime.january];
return '$month ${date.day}, ${date.year}';
}
@override
String formatMediumDate(DateTime date) {
final String day = _shortWeekdays[date.weekday - DateTime.monday];
@ -490,6 +563,37 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return '$month $year';
}
@override
String formatShortMonthDay(DateTime date) {
final String month = _shortMonths[date.month - DateTime.january];
return '$month ${date.day}';
}
@override
DateTime parseCompactDate(String inputString) {
// Assumes US mm/dd/yyyy format
final List<String> inputParts = inputString.split('/');
if (inputParts.length != 3) {
return null;
}
final int year = int.tryParse(inputParts[2], radix: 10);
if (year == null || year < 1) {
return null;
}
final int month = int.tryParse(inputParts[0], radix: 10);
if (month == null || month < 1 || month > 12) {
return null;
}
final int day = int.tryParse(inputParts[1], radix: 10);
if (day == null || day < 1 || day > _getDaysInMonth(year, month)) {
return null;
}
return DateTime(year, month, day);
}
@override
List<String> get narrowWeekdays => _narrowWeekdays;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
// 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.
/// 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.
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].
typedef SelectableDayPredicate = bool Function(DateTime day);

View file

@ -5,188 +5,36 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import '../debug.dart';
import '../icon_button.dart';
import '../icons.dart';
import '../ink_well.dart';
import '../material.dart';
import '../material_localizations.dart';
import '../theme.dart';
import 'date_picker_common.dart';
// NOTE: this is the original implementation for the Material Date Picker.
// These classes are deprecated and the whole file can be removed after
// this has been on stable for long enough for people to migrate to the new
// CalendarDatePicker (if needed, as showDatePicker has already been migrated
// and it is what most apps would have used).
import 'button_bar.dart';
import 'colors.dart';
import 'debug.dart';
import 'dialog.dart';
import 'feedback.dart';
import 'flat_button.dart';
import 'icon_button.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'text_theme.dart';
import 'theme.dart';
// Examples can assume:
// BuildContext context;
/// Initial display mode of the date picker dialog.
///
/// Date picker UI mode for either showing a list of available years or a
/// monthly calendar initially in the dialog shown by calling [showDatePicker].
///
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker.
enum DatePickerMode {
/// Show a date picker UI for choosing a month and day.
day,
/// Show a date picker UI for choosing a year.
year,
}
const Duration _kMonthScrollDuration = Duration(milliseconds: 200);
const double _kDayPickerRowHeight = 42.0;
const int _kMaxDayPickerRowCount = 6; // A 31 day month that starts on Saturday.
// Two extra rows: one for the day-of-week header and one for the month header.
const double _kMaxDayPickerHeight = _kDayPickerRowHeight * (_kMaxDayPickerRowCount + 2);
// Shows the selected date in large font and toggles between year and day mode
class _DatePickerHeader extends StatelessWidget {
const _DatePickerHeader({
Key key,
@required this.selectedDate,
@required this.mode,
@required this.onModeChanged,
@required this.orientation,
}) : assert(selectedDate != null),
assert(mode != null),
assert(orientation != null),
super(key: key);
final DateTime selectedDate;
final DatePickerMode mode;
final ValueChanged<DatePickerMode> onModeChanged;
final Orientation orientation;
void _handleChangeMode(DatePickerMode value) {
if (value != mode)
onModeChanged(value);
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
final TextTheme headerTextTheme = themeData.primaryTextTheme;
Color dayColor;
Color yearColor;
switch (themeData.primaryColorBrightness) {
case Brightness.light:
dayColor = mode == DatePickerMode.day ? Colors.black87 : Colors.black54;
yearColor = mode == DatePickerMode.year ? Colors.black87 : Colors.black54;
break;
case Brightness.dark:
dayColor = mode == DatePickerMode.day ? Colors.white : Colors.white70;
yearColor = mode == DatePickerMode.year ? Colors.white : Colors.white70;
break;
}
final TextStyle dayStyle = headerTextTheme.headline4.copyWith(color: dayColor);
final TextStyle yearStyle = headerTextTheme.subtitle1.copyWith(color: yearColor);
Color backgroundColor;
switch (themeData.brightness) {
case Brightness.light:
backgroundColor = themeData.primaryColor;
break;
case Brightness.dark:
backgroundColor = themeData.backgroundColor;
break;
}
EdgeInsets padding;
MainAxisAlignment mainAxisAlignment;
switch (orientation) {
case Orientation.portrait:
padding = const EdgeInsets.all(16.0);
mainAxisAlignment = MainAxisAlignment.center;
break;
case Orientation.landscape:
padding = const EdgeInsets.all(8.0);
mainAxisAlignment = MainAxisAlignment.start;
break;
}
final Widget yearButton = IgnorePointer(
ignoring: mode != DatePickerMode.day,
ignoringSemantics: false,
child: _DateHeaderButton(
color: backgroundColor,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.year), context),
child: Semantics(
selected: mode == DatePickerMode.year,
child: Text(localizations.formatYear(selectedDate), style: yearStyle),
),
),
);
final Widget dayButton = IgnorePointer(
ignoring: mode == DatePickerMode.day,
ignoringSemantics: false,
child: _DateHeaderButton(
color: backgroundColor,
onTap: Feedback.wrapForTap(() => _handleChangeMode(DatePickerMode.day), context),
child: Semantics(
selected: mode == DatePickerMode.day,
child: Text(localizations.formatMediumDate(selectedDate), style: dayStyle),
),
),
);
return Container(
padding: padding,
color: backgroundColor,
child: Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[yearButton, dayButton],
),
);
}
}
class _DateHeaderButton extends StatelessWidget {
const _DateHeaderButton({
Key key,
this.onTap,
this.color,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final Color color;
final Widget child;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Material(
type: MaterialType.button,
color: color,
child: InkWell(
borderRadius: kMaterialEdges[MaterialType.button],
highlightColor: theme.highlightColor,
splashColor: theme.splashColor,
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: child,
),
),
);
}
}
class _DayPickerGridDelegate extends SliverGridDelegate {
const _DayPickerGridDelegate();
@ -226,6 +74,11 @@ const _DayPickerGridDelegate _kDayPickerGridDelegate = _DayPickerGridDelegate();
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class DayPicker extends StatelessWidget {
/// Creates a day picker.
///
@ -503,6 +356,11 @@ class DayPicker extends StatelessWidget {
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class MonthPicker extends StatefulWidget {
/// Creates a month picker.
///
@ -546,6 +404,7 @@ class MonthPicker extends StatefulWidget {
_MonthPickerState createState() => _MonthPickerState();
}
// ignore: deprecated_member_use_from_same_package
class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStateMixin {
static final Animatable<double> _chevronOpacityTween = Tween<double>(begin: 1.0, end: 0.0)
.chain(CurveTween(curve: Curves.easeInOut));
@ -567,6 +426,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
}
@override
// ignore: deprecated_member_use_from_same_package
void didUpdateWidget(MonthPicker oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
@ -617,6 +477,7 @@ class _MonthPickerState extends State<MonthPicker> with SingleTickerProviderStat
Widget _buildItems(BuildContext context, int index) {
final DateTime month = _addMonthsToMonthDate(widget.firstDate, index);
// ignore: deprecated_member_use_from_same_package
return DayPicker(
key: ValueKey<DateTime>(month),
selectedDate: widget.selectedDate,
@ -766,6 +627,11 @@ class _MonthPickerSortKey extends OrdinalSortKey {
/// date picker.
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
///
@Deprecated(
'Use CalendarDatePicker instead. '
'This feature was deprecated after v1.15.3.'
)
class YearPicker extends StatefulWidget {
/// Creates a year picker.
///
@ -807,6 +673,7 @@ class YearPicker extends StatefulWidget {
_YearPickerState createState() => _YearPickerState();
}
// ignore: deprecated_member_use_from_same_package
class _YearPickerState extends State<YearPicker> {
static const double _itemExtent = 50.0;
ScrollController scrollController;
@ -852,339 +719,3 @@ class _YearPickerState extends State<YearPicker> {
);
}
}
class _DatePickerDialog extends StatefulWidget {
const _DatePickerDialog({
Key key,
this.initialDate,
this.firstDate,
this.lastDate,
this.selectableDayPredicate,
this.initialDatePickerMode,
}) : super(key: key);
final DateTime initialDate;
final DateTime firstDate;
final DateTime lastDate;
final SelectableDayPredicate selectableDayPredicate;
final DatePickerMode initialDatePickerMode;
@override
_DatePickerDialogState createState() => _DatePickerDialogState();
}
class _DatePickerDialogState extends State<_DatePickerDialog> {
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate;
_mode = widget.initialDatePickerMode;
}
bool _announcedInitialDate = false;
MaterialLocalizations localizations;
TextDirection textDirection;
@override
void didChangeDependencies() {
super.didChangeDependencies();
localizations = MaterialLocalizations.of(context);
textDirection = Directionality.of(context);
if (!_announcedInitialDate) {
_announcedInitialDate = true;
SemanticsService.announce(
localizations.formatFullDate(_selectedDate),
textDirection,
);
}
}
DateTime _selectedDate;
DatePickerMode _mode;
final GlobalKey _pickerKey = GlobalKey();
void _vibrate() {
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
HapticFeedback.vibrate();
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
break;
}
}
void _handleModeChanged(DatePickerMode mode) {
_vibrate();
setState(() {
_mode = mode;
if (_mode == DatePickerMode.day) {
SemanticsService.announce(localizations.formatMonthYear(_selectedDate), textDirection);
} else {
SemanticsService.announce(localizations.formatYear(_selectedDate), textDirection);
}
});
}
void _handleYearChanged(DateTime value) {
if (value.isBefore(widget.firstDate))
value = widget.firstDate;
else if (value.isAfter(widget.lastDate))
value = widget.lastDate;
if (value == _selectedDate)
return;
_vibrate();
setState(() {
_mode = DatePickerMode.day;
_selectedDate = value;
});
}
void _handleDayChanged(DateTime value) {
_vibrate();
setState(() {
_selectedDate = value;
});
}
void _handleCancel() {
Navigator.pop(context);
}
void _handleOk() {
Navigator.pop(context, _selectedDate);
}
Widget _buildPicker() {
assert(_mode != null);
switch (_mode) {
case DatePickerMode.day:
return MonthPicker(
key: _pickerKey,
selectedDate: _selectedDate,
onChanged: _handleDayChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
selectableDayPredicate: widget.selectableDayPredicate,
);
case DatePickerMode.year:
return YearPicker(
key: _pickerKey,
selectedDate: _selectedDate,
onChanged: _handleYearChanged,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
);
}
return null;
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final Widget picker = _buildPicker();
final Widget actions = ButtonBar(
children: <Widget>[
FlatButton(
child: Text(localizations.cancelButtonLabel),
onPressed: _handleCancel,
),
FlatButton(
child: Text(localizations.okButtonLabel),
onPressed: _handleOk,
),
],
);
final Dialog dialog = Dialog(
child: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) {
assert(orientation != null);
final Widget header = _DatePickerHeader(
selectedDate: _selectedDate,
mode: _mode,
onModeChanged: _handleModeChanged,
orientation: orientation,
);
switch (orientation) {
case Orientation.portrait:
return Container(
color: theme.dialogBackgroundColor,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
header,
Flexible(child: picker),
actions,
],
),
);
case Orientation.landscape:
return Container(
color: theme.dialogBackgroundColor,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(child: header),
Flexible(
flex: 2, // have the picker take up 2/3 of the dialog width
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(child: picker),
actions,
],
),
),
],
),
);
}
return null;
}
),
);
return Theme(
data: theme.copyWith(
dialogBackgroundColor: Colors.transparent,
),
child: dialog,
);
}
}
/// Signature for predicating dates for enabled date selections.
///
/// See [showDatePicker].
typedef SelectableDayPredicate = bool Function(DateTime day);
/// Shows a dialog containing a material design date picker.
///
/// The returned [Future] resolves to the date selected by the user when the
/// user closes the dialog. If the user cancels the dialog, null is returned.
///
/// An optional [selectableDayPredicate] function can be passed in to customize
/// the days to enable for selection. If provided, only the days that
/// [selectableDayPredicate] returned true for will be selectable.
///
/// An optional [initialDatePickerMode] argument can be used to display the
/// date picker initially in the year or month+day picker mode. It defaults
/// to month+day, and must not be null.
///
/// 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
/// (RTL or LTR) for the date picker. It defaults to the ambient text direction
/// provided by [Directionality]. If both [locale] and [textDirection] are not
/// 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.
///
/// The [builder] parameter can be used to wrap the dialog widget
/// to add inherited widgets like [Theme].
///
/// {@animation 350 622 https://flutter.github.io/assets-for-api-docs/assets/material/show_date_picker.mp4}
///
/// {@tool snippet}
/// Show a date picker with the dark theme.
///
/// ```dart
/// Future<DateTime> selectedDate = showDatePicker(
/// context: context,
/// initialDate: DateTime.now(),
/// firstDate: DateTime(2018),
/// lastDate: DateTime(2030),
/// builder: (BuildContext context, Widget child) {
/// return Theme(
/// data: ThemeData.dark(),
/// child: child,
/// );
/// },
/// );
/// ```
/// {@end-tool}
///
/// The [context], [initialDate], [firstDate], and [lastDate] parameters must
/// not be null.
///
/// See also:
///
/// * [showTimePicker], which shows a dialog that contains a material design
/// time picker.
/// * [DayPicker], which displays the days of a given month and allows
/// choosing a day.
/// * [MonthPicker], which displays a scrollable list of months to allow
/// picking a month.
/// * [YearPicker], which displays a scrollable list of years to allow picking
/// a year.
Future<DateTime> showDatePicker({
@required BuildContext context,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
SelectableDayPredicate selectableDayPredicate,
DatePickerMode initialDatePickerMode = DatePickerMode.day,
Locale locale,
TextDirection textDirection,
TransitionBuilder builder,
bool useRootNavigator = true,
RouteSettings routeSettings,
}) async {
assert(initialDate != null);
assert(firstDate != null);
assert(lastDate != null);
assert(useRootNavigator != null);
assert(!initialDate.isBefore(firstDate), 'initialDate must be on or after firstDate');
assert(!initialDate.isAfter(lastDate), 'initialDate must be on or before lastDate');
assert(!firstDate.isAfter(lastDate), 'lastDate must be on or after firstDate');
assert(
selectableDayPredicate == null || selectableDayPredicate(initialDate),
'Provided initialDate must satisfy provided selectableDayPredicate'
);
assert(initialDatePickerMode != null, 'initialDatePickerMode must not be null');
assert(context != null);
assert(debugCheckHasMaterialLocalizations(context));
Widget child = _DatePickerDialog(
initialDate: initialDate,
firstDate: firstDate,
lastDate: lastDate,
selectableDayPredicate: selectableDayPredicate,
initialDatePickerMode: initialDatePickerMode,
);
if (textDirection != null) {
child = Directionality(
textDirection: textDirection,
child: child,
);
}
if (locale != null) {
child = Localizations.override(
context: context,
locale: locale,
child: child,
);
}
return await showDialog<DateTime>(
context: context,
useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return builder == null ? child : builder(context, child);
},
routeSettings: routeSettings,
);
}

View file

@ -0,0 +1,482 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import '../button_bar.dart';
import '../button_theme.dart';
import '../color_scheme.dart';
import '../debug.dart';
import '../dialog.dart';
import '../flat_button.dart';
import '../icons.dart';
import '../material_localizations.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);
/// 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.
///
/// 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].
///
/// Optional strings for the [cancelText], [confirmText], [errorFormatText],
/// [errorInvalidText], [fieldHintText], [fieldLabelText], and [helpText] allow
/// you to override the default text used for various parts 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.
/// * [helpText], label on the top of the dialog.
///
/// 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.
Future<DateTime> showDatePicker({
@required BuildContext context,
@required DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
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,
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,
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),
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;
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> {
DatePickerEntryMode _entryMode;
DateTime _selectedDate;
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 _handelEntryModeToggle() {
setState(() {
switch (_entryMode) {
case DatePickerEntryMode.calendar:
_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;
}
break;
case DatePickerEntryMode.input:
switch (orientation) {
case Orientation.portrait:
return _inputPortraitDialogSize;
case Orientation.landscape:
return _inputLandscapeDialogSize;
}
break;
}
return null;
}
@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 = _selectedDate != null
? localizations.formatMediumDate(_selectedDate)
// TODO(darrenaustin): localize 'Date'
: 'Date';
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 = ButtonBar(
buttonTextTheme: ButtonTextTheme.primary,
layoutBehavior: ButtonBarLayoutBehavior.constrained,
children: <Widget>[
FlatButton(
child: Text(widget.cancelText ?? localizations.cancelButtonLabel),
onPressed: _handleCancel,
),
FlatButton(
child: Text(widget.confirmText ?? localizations.okButtonLabel),
onPressed: _handleOk,
),
],
);
Widget picker;
IconData entryModeIcon;
String entryModeTooltip;
switch (_entryMode) {
case DatePickerEntryMode.calendar:
picker = CalendarDatePicker(
key: _calendarPickerKey,
initialDate: _selectedDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
onDateChanged: _handleDateChanged,
selectableDayPredicate: widget.selectableDayPredicate,
initialCalendarMode: widget.initialCalendarMode,
);
entryModeIcon = Icons.edit;
// TODO(darrenaustin): localize 'Switch to input'
entryModeTooltip = 'Switch to input';
break;
case DatePickerEntryMode.input:
picker = Form(
key: _formKey,
autovalidate: _autoValidate,
child: 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,
),
);
entryModeIcon = Icons.calendar_today;
// TODO(darrenaustin): localize 'Switch to calendar'
entryModeTooltip = 'Switch to calendar';
break;
}
final Widget header = DatePickerHeader(
// TODO(darrenaustin): localize 'SELECT DATE'
helpText: widget.helpText ?? 'SELECT DATE',
titleText: dateText,
titleStyle: dateStyle,
orientation: orientation,
isShort: orientation == Orientation.landscape,
icon: entryModeIcon,
iconTooltip: entryModeTooltip,
onIconPressed: _handelEntryModeToggle,
);
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,
],
),
),
],
);
}
return null;
}),
),
),
insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0))
),
clipBehavior: Clip.antiAlias,
elevation: 24.0,
);
}
}

View file

@ -0,0 +1,193 @@
// 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 '../text_theme.dart';
import '../theme.dart';
// NOTE: 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.
///
/// [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: (isShort || 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 Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
height: _datePickerHeaderPortraitHeight,
color: primarySurfaceColor,
padding: const EdgeInsetsDirectional.only(
start: 24,
end: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 16),
Flexible(child: help),
const SizedBox(height: 38),
Row(
children: <Widget>[
Expanded(child: title),
icon,
],
),
],
),
),
],
);
case Orientation.landscape:
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: _datePickerHeaderLandscapeWidth,
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),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _headerPaddingLandscape,
),
child: title,
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: icon,
),
],
),
),
],
);
}
return null;
}
}

View file

@ -0,0 +1,122 @@
// 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
// NOTE: 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';
/// 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 true if the two [DateTime] objects have the same day, month, and
/// year.
bool isSameDay(DateTime dateA, DateTime dateB) {
return
dateA.year == dateB.year &&
dateA.month == dateB.month &&
dateA.day == dateB.day;
}
/// 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);
}
/// 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];
}

View file

@ -0,0 +1,281 @@
// 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 '../text_form_field.dart';
import 'date_picker_common.dart';
import 'date_utils.dart' as utils;
const double _inputPortraitHeight = 98.0;
const double _inputLandscapeHeight = 108.0;
/// A [TextFormField] configured to accept and validate a date entered by the user.
///
/// The text entered into this field will be constrained to only allow digits
/// and separators. When saved or submitted, the text will be parsed into a
/// [DateTime] according to the ambient locale. If the input text doesn't parse
/// into a date, the [errorFormatText] message will be displayed under the field.
///
/// [firstDate], [lastDate], and [selectableDayPredicate] provide constraints on
/// what days are valid. If the input date isn't in the date range or doesn't pass
/// the given predicate, then the [errorInvalidText] message will be displayed
/// under the field.
///
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker which includes support for text entry of dates.
/// * [MaterialLocalizations.parseCompactDate], which is used to parse the text
/// input into a [DateTime].
///
class InputDatePickerFormField extends StatefulWidget {
/// Creates a [TextFormField] configured to accept and validate a date.
///
/// If the optional [initialDate] is provided, then it will be used to populate
/// the text field. If the [fieldHintText] is provided, it will be shown.
///
/// If [initialDate] is provided, it must not be before [firstDate] or after
/// [lastDate]. If [selectableDayPredicate] is provided, it must return `true`
/// for [initialDate].
///
/// [firstDate] must be on or before [lastDate].
///
/// [firstDate], [lastDate], and [autofocus] must be non-null.
///
InputDatePickerFormField({
Key key,
DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
this.onDateSubmitted,
this.onDateSaved,
this.selectableDayPredicate,
this.errorFormatText,
this.errorInvalidText,
this.fieldHintText,
this.fieldLabelText,
this.autofocus = false,
}) : assert(firstDate != null),
assert(lastDate != null),
assert(autofocus != null),
initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isBefore(this.firstDate),
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isAfter(this.lastDate),
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
);
assert(
selectableDayPredicate == null || initialDate == null || selectableDayPredicate(this.initialDate),
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.'
);
}
/// If provided, it will be used as the default value of the field.
final DateTime initialDate;
/// The earliest allowable [DateTime] that the user can input.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can input.
final DateTime lastDate;
/// An optional method to call when the user indicates they are done editing
/// the text in the field. Will only be called if the input represents a valid
/// [DateTime].
final ValueChanged<DateTime> onDateSubmitted;
/// An optional method to call with the final date when the form is
/// saved via [FormState.save]. Will only be called if the input represents
/// a valid [DateTime].
final ValueChanged<DateTime> onDateSaved;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayPredicate selectableDayPredicate;
/// The error text displayed if the entered date is not in the correct format.
final String errorFormatText;
/// The error text displayed if the date is not valid.
///
/// A date is not valid if it is earlier than [firstDate], later than
/// [lastDate], or doesn't pass the [selectableDayPredicate].
final String errorInvalidText;
/// The hint text displayed in the [TextField].
///
/// If this is null, it will default to the date format string. For example,
/// 'mm/dd/yyyy' for en_US.
final String fieldHintText;
/// The label text displayed in the [TextField].
///
/// If this is null, it will default to the words representing the date format
/// string. For example, 'Month, Day, Year' for en_US.
final String fieldLabelText;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
@override
_InputDatePickerFormFieldState createState() => _InputDatePickerFormFieldState();
}
class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
final TextEditingController _controller = TextEditingController();
DateTime _selectedDate;
String _inputText;
bool _autoSelected = false;
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_selectedDate != null) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
_inputText = localizations.formatCompactDate(_selectedDate);
TextEditingValue textEditingValue = _controller.value.copyWith(text: _inputText);
// Select the new text if we are auto focused and haven't selected the text before.
if (widget.autofocus && !_autoSelected) {
textEditingValue = textEditingValue.copyWith(selection: TextSelection(
baseOffset: 0,
extentOffset: _inputText.length,
));
_autoSelected = true;
}
_controller.value = textEditingValue;
}
}
DateTime _parseDate(String text) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.parseCompactDate(text);
}
bool _isValidAcceptableDate(DateTime date) {
return
date != null &&
!date.isBefore(widget.firstDate) &&
!date.isAfter(widget.lastDate) &&
(widget.selectableDayPredicate == null || widget.selectableDayPredicate(date));
}
String _validateDate(String text) {
final DateTime date = _parseDate(text);
if (date == null) {
// TODO(darrenaustin): localize 'Invalid format.'
return widget.errorFormatText ?? 'Invalid format.';
} else if (!_isValidAcceptableDate(date)) {
// TODO(darrenaustin): localize 'Out of range.'
return widget.errorInvalidText ?? 'Out of range.';
}
return null;
}
void _handleSaved(String text) {
if (widget.onDateSaved != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSaved(date);
}
}
}
void _handleSubmitted(String text) {
if (widget.onDateSubmitted != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSubmitted(date);
}
}
}
@override
Widget build(BuildContext context) {
return OrientationBuilder(builder: (BuildContext context, Orientation orientation) {
assert(orientation != null);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24),
height: orientation == Orientation.portrait ? _inputPortraitHeight : _inputLandscapeHeight,
child: Column(
children: <Widget>[
const Spacer(),
TextFormField(
decoration: InputDecoration(
border: const UnderlineInputBorder(),
filled: true,
// TODO(darrenaustin): localize 'mm/dd/yyyy' and 'Enter Date'
hintText: widget.fieldHintText ?? 'mm/dd/yyyy',
labelText: widget.fieldLabelText ?? 'Enter Date',
),
validator: _validateDate,
inputFormatters: <TextInputFormatter>[
// TODO(darrenaustin): localize date separator '/'
_DateTextInputFormatter('/'),
],
keyboardType: TextInputType.datetime,
onSaved: _handleSaved,
onFieldSubmitted: _handleSubmitted,
autofocus: widget.autofocus,
controller: _controller,
),
const Spacer(),
],
),
);
});
}
}
class _DateTextInputFormatter extends TextInputFormatter {
_DateTextInputFormatter(this.separator);
final String separator;
final WhitelistingTextInputFormatter _filterFormatter =
// Only allow digits and separators (slash, dot, comma, hyphen, space).
WhitelistingTextInputFormatter(RegExp(r'[\d\/\.,-\s]+'));
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final TextEditingValue filteredValue = _filterFormatter.formatEditUpdate(oldValue, newValue);
return filteredValue.copyWith(
// Replace any separator character with the given separator
text: filteredValue.text.replaceAll(RegExp(r'[\D]'), separator),
);
}
}

View file

@ -0,0 +1,10 @@
// 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, SelectableDayPredicate;
export 'date_picker_deprecated.dart';
export 'date_picker_dialog.dart' show showDatePicker;
export 'input_date_picker.dart' show InputDatePickerFormField;

File diff suppressed because it is too large Load diff

View file

@ -76,12 +76,14 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
/// 1. The string that would be returned by [Intl.canonicalizedLocale] for
/// the locale.
/// 2. The [intl.DateFormat] for [formatYear].
/// 3. The [intl.DateFormat] for [formatMediumDate].
/// 4. The [intl.DateFormat] for [formatFullDate].
/// 5. The [intl.DateFormat] for [formatMonthYear].
/// 6. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
/// 3. The [int.DateFormat] for [formatShortDate].
/// 4. The [intl.DateFormat] for [formatMediumDate].
/// 5. The [intl.DateFormat] for [formatFullDate].
/// 6. The [intl.DateFormat] for [formatMonthYear].
/// 7. The [int.DateFormat] for [formatShortMonthDay].
/// 8. The [NumberFormat] for [formatDecimal] (also used by [formatHour] and
/// [formatTimeOfDay] when [timeOfDayFormat] doesn't use [HourFormat.HH]).
/// 7. The [NumberFormat] for [formatHour] and the hour part of
/// 9. The [NumberFormat] for [formatHour] and the hour part of
/// [formatTimeOfDay] when [timeOfDayFormat] uses [HourFormat.HH], and for
/// [formatMinute] and the minute part of [formatTimeOfDay].
///
@ -90,21 +92,30 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
const GlobalMaterialLocalizations({
@required String localeName,
@required intl.DateFormat fullYearFormat,
@required intl.DateFormat compactDateFormat,
@required intl.DateFormat shortDateFormat,
@required intl.DateFormat mediumDateFormat,
@required intl.DateFormat longDateFormat,
@required intl.DateFormat yearMonthFormat,
@required intl.DateFormat shortMonthDayFormat,
@required intl.NumberFormat decimalFormat,
@required intl.NumberFormat twoDigitZeroPaddedFormat,
}) : assert(localeName != null),
_localeName = localeName,
assert(fullYearFormat != null),
_fullYearFormat = fullYearFormat,
assert(compactDateFormat != null),
_compactDateFormat = compactDateFormat,
assert(shortDateFormat != null),
_shortDateFormat = shortDateFormat,
assert(mediumDateFormat != null),
_mediumDateFormat = mediumDateFormat,
assert(longDateFormat != null),
_longDateFormat = longDateFormat,
assert(yearMonthFormat != null),
_yearMonthFormat = yearMonthFormat,
assert(shortMonthDayFormat != null),
_shortMonthDayFormat = shortMonthDayFormat,
assert(decimalFormat != null),
_decimalFormat = decimalFormat,
assert(twoDigitZeroPaddedFormat != null),
@ -112,9 +123,12 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
final String _localeName;
final intl.DateFormat _fullYearFormat;
final intl.DateFormat _compactDateFormat;
final intl.DateFormat _shortDateFormat;
final intl.DateFormat _mediumDateFormat;
final intl.DateFormat _longDateFormat;
final intl.DateFormat _yearMonthFormat;
final intl.DateFormat _shortMonthDayFormat;
final intl.NumberFormat _decimalFormat;
final intl.NumberFormat _twoDigitZeroPaddedFormat;
@ -142,6 +156,16 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
return _fullYearFormat.format(date);
}
@override
String formatCompactDate(DateTime date) {
return _compactDateFormat.format(date);
}
@override
String formatShortDate(DateTime date) {
return _shortDateFormat.format(date);
}
@override
String formatMediumDate(DateTime date) {
return _mediumDateFormat.format(date);
@ -157,6 +181,20 @@ abstract class GlobalMaterialLocalizations implements MaterialLocalizations {
return _yearMonthFormat.format(date);
}
@override
String formatShortMonthDay(DateTime date) {
return _shortMonthDayFormat.format(date);
}
@override
DateTime parseCompactDate(String inputString) {
try {
return _compactDateFormat.parseStrict(inputString);
} on FormatException {
return null;
}
}
@override
List<String> get narrowWeekdays {
return _longDateFormat.dateSymbols.NARROWWEEKDAYS;
@ -576,24 +614,36 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
);
intl.DateFormat fullYearFormat;
intl.DateFormat compactDateFormat;
intl.DateFormat shortDateFormat;
intl.DateFormat mediumDateFormat;
intl.DateFormat longDateFormat;
intl.DateFormat yearMonthFormat;
intl.DateFormat shortMonthDayFormat;
if (intl.DateFormat.localeExists(localeName)) {
fullYearFormat = intl.DateFormat.y(localeName);
compactDateFormat = intl.DateFormat.yMd(localeName);
shortDateFormat = intl.DateFormat.yMMMd(localeName);
mediumDateFormat = intl.DateFormat.MMMEd(localeName);
longDateFormat = intl.DateFormat.yMMMMEEEEd(localeName);
yearMonthFormat = intl.DateFormat.yMMMM(localeName);
shortMonthDayFormat = intl.DateFormat.MMMd(localeName);
} else if (intl.DateFormat.localeExists(locale.languageCode)) {
fullYearFormat = intl.DateFormat.y(locale.languageCode);
compactDateFormat = intl.DateFormat.yMd(locale.languageCode);
shortDateFormat = intl.DateFormat.yMMMd(locale.languageCode);
mediumDateFormat = intl.DateFormat.MMMEd(locale.languageCode);
longDateFormat = intl.DateFormat.yMMMMEEEEd(locale.languageCode);
yearMonthFormat = intl.DateFormat.yMMMM(locale.languageCode);
shortMonthDayFormat = intl.DateFormat.MMMd(locale.languageCode);
} else {
fullYearFormat = intl.DateFormat.y();
compactDateFormat = intl.DateFormat.yMd();
shortDateFormat = intl.DateFormat.yMMMd();
mediumDateFormat = intl.DateFormat.MMMEd();
longDateFormat = intl.DateFormat.yMMMMEEEEd();
yearMonthFormat = intl.DateFormat.yMMMM();
shortMonthDayFormat = intl.DateFormat.MMMd();
}
intl.NumberFormat decimalFormat;
@ -612,9 +662,12 @@ class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocal
return SynchronousFuture<MaterialLocalizations>(getMaterialTranslation(
locale,
fullYearFormat,
compactDateFormat,
shortDateFormat,
mediumDateFormat,
longDateFormat,
yearMonthFormat,
shortMonthDayFormat,
decimalFormat,
twoDigitZeroPaddedFormat,
));

View file

@ -19,7 +19,7 @@ void main() {
initialDate = DateTime(2016, DateTime.january, 15);
});
group(DayPicker, () {
group(CalendarDatePicker, () {
final intl.NumberFormat arabicNumbers = intl.NumberFormat('0', 'ar');
final Map<Locale, Map<String, dynamic>> testLocales = <Locale, Map<String, dynamic>>{
// Tests the default.
@ -59,13 +59,11 @@ void main() {
final TextDirection textDirection = testLocales[locale]['textDirection'] as TextDirection;
final DateTime baseDate = DateTime(2017, 9, 27);
await _pumpBoilerplate(tester, DayPicker(
selectedDate: baseDate,
currentDate: baseDate,
onChanged: (DateTime newValue) { },
await _pumpBoilerplate(tester, CalendarDatePicker(
initialDate: baseDate,
firstDate: baseDate.subtract(const Duration(days: 90)),
lastDate: baseDate.add(const Duration(days: 90)),
displayedMonth: baseDate,
onDateChanged: (DateTime newValue) {},
), locale: locale, textDirection: textDirection);
expect(find.text(expectedMonthYearHeader), findsOneWidget);
@ -126,14 +124,14 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Localizations.localeOf(dayPicker),
Localizations.localeOf(picker),
const Locale('fr', 'CA'),
);
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.ltr,
);
@ -169,9 +167,9 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.rtl,
);
@ -210,14 +208,14 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Element dayPicker = tester.element(find.byType(DayPicker));
final Element picker = tester.element(find.byType(CalendarDatePicker));
expect(
Localizations.localeOf(dayPicker),
Localizations.localeOf(picker),
const Locale('fr', 'CA'),
);
expect(
Directionality.of(dayPicker),
Directionality.of(picker),
TextDirection.rtl,
);
@ -292,12 +290,16 @@ Future<void> _pumpBoilerplate(
Locale locale = const Locale('en', 'US'),
TextDirection textDirection = TextDirection.ltr,
}) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: locale,
delegates: GlobalMaterialLocalizations.delegates,
child: child,
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: locale,
delegates: GlobalMaterialLocalizations.delegates,
child: Material(
child: child,
),
),
),
));
}

View file

@ -15,9 +15,12 @@ class FooMaterialLocalizations extends MaterialLocalizationEn {
) : super(
localeName: localeName.toString(),
fullYearFormat: intl.DateFormat.y(),
compactDateFormat: intl.DateFormat.yMd(),
shortDateFormat: intl.DateFormat.yMMMd(),
mediumDateFormat: intl.DateFormat('E, MMM\u00a0d'),
longDateFormat: intl.DateFormat.yMMMMEEEEd(),
yearMonthFormat: intl.DateFormat.yMMMM(),
shortMonthDayFormat: intl.DateFormat.MMMd(),
decimalFormat: intl.NumberFormat.decimalPattern(),
twoDigitZeroPaddedFormat: intl.NumberFormat('00'),
);