mirror of
https://github.com/flutter/flutter
synced 2024-09-17 23:31:55 +00:00
parent
25ef78e234
commit
142b526f1b
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
1043
packages/flutter/lib/src/material/pickers/calendar_date_picker.dart
Normal file
1043
packages/flutter/lib/src/material/pickers/calendar_date_picker.dart
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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);
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
122
packages/flutter/lib/src/material/pickers/date_utils.dart
Normal file
122
packages/flutter/lib/src/material/pickers/date_utils.dart
Normal 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];
|
||||
}
|
281
packages/flutter/lib/src/material/pickers/input_date_picker.dart
Normal file
281
packages/flutter/lib/src/material/pickers/input_date_picker.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
10
packages/flutter/lib/src/material/pickers/pickers.dart
Normal file
10
packages/flutter/lib/src/material/pickers/pickers.dart
Normal 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
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
));
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue