Unverified Commit c5f8edd9 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Material Date Picker code restructure (#70708)

Date Picker restructuring. Moved files out of 'pickers' and merged several of the non-public implementation files into date_picker.dart.
parent d4b871e1
......@@ -37,6 +37,7 @@ export 'src/material/button_bar_theme.dart';
export 'src/material/button_style.dart';
export 'src/material/button_style_button.dart';
export 'src/material/button_theme.dart';
export 'src/material/calendar_date_picker.dart';
export 'src/material/card.dart';
export 'src/material/card_theme.dart';
export 'src/material/checkbox.dart';
......@@ -51,6 +52,9 @@ export 'src/material/curves.dart';
export 'src/material/data_table.dart';
export 'src/material/data_table_source.dart';
export 'src/material/data_table_theme.dart';
export 'src/material/date.dart';
export 'src/material/date_picker.dart';
export 'src/material/date_picker_deprecated.dart';
export 'src/material/debug.dart';
export 'src/material/dialog.dart';
export 'src/material/dialog_theme.dart';
......@@ -82,6 +86,7 @@ export 'src/material/ink_ripple.dart';
export 'src/material/ink_splash.dart';
export 'src/material/ink_well.dart';
export 'src/material/input_border.dart';
export 'src/material/input_date_picker_form_field.dart';
export 'src/material/input_decorator.dart';
export 'src/material/list_tile.dart';
export 'src/material/material.dart';
......@@ -97,7 +102,6 @@ export 'src/material/outlined_button_theme.dart';
export 'src/material/page.dart';
export 'src/material/page_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';
......
......@@ -6,6 +6,144 @@ import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart';
import 'material_localizations.dart';
/// Utility functions for working with dates.
class DateUtils {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
DateUtils._();
/// Returns a [DateTime] with the date of the original, but time set to
/// midnight.
static DateTime dateOnly(DateTime date) {
return DateTime(date.year, date.month, date.day);
}
/// Returns a [DateTimeRange] with the dates of the original, but with times
/// set to midnight.
///
/// See also:
/// * [dateOnly], which does the same thing for a single date.
static DateTimeRange datesOnly(DateTimeRange range) {
return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end));
}
/// Returns true if the two [DateTime] objects have the same day, month, and
/// year, or are both null.
static bool isSameDay(DateTime? dateA, DateTime? dateB) {
return
dateA?.year == dateB?.year &&
dateA?.month == dateB?.month &&
dateA?.day == dateB?.day;
}
/// Returns true if the two [DateTime] objects have the same month and
/// year, or are both null.
static bool isSameMonth(DateTime? dateA, DateTime? dateB) {
return
dateA?.year == dateB?.year &&
dateA?.month == dateB?.month;
}
/// Determines the number of months between two [DateTime] objects.
///
/// For example:
/// ```
/// DateTime date1 = DateTime(year: 2019, month: 6, day: 15);
/// DateTime date2 = DateTime(year: 2020, month: 1, day: 15);
/// int delta = monthDelta(date1, date2);
/// ```
///
/// The value for `delta` would be `7`.
static int monthDelta(DateTime startDate, DateTime endDate) {
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
}
/// Returns a [DateTime] that is [monthDate] with the added number
/// of months and the day set to 1 and time set to midnight.
///
/// For example:
/// ```
/// DateTime date = DateTime(year: 2019, month: 1, day: 15);
/// DateTime futureDate = DateUtils.addMonthsToMonthDate(date, 3);
/// ```
///
/// `date` would be January 15, 2019.
/// `futureDate` would be April 1, 2019 since it adds 3 months.
static DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
}
/// Returns a [DateTime] with the added number of days and time set to
/// midnight.
static DateTime addDaysToDate(DateTime date, int days) {
return DateTime(date.year, date.month, date.day + days);
}
/// Computes the offset from the first day of the week that the first day of
/// the [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
/// into the [MaterialLocalizations.narrowWeekdays] list.
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
static int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
// 0-based day of week for the month and year, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// 0-based start of week depending on the locale, with 0 representing Sunday.
int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex;
// firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with
// weekdayFromMonday.
firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the first of the month.
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
}
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
static int getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0);
return isLeapYear ? 29 : 28;
}
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return daysInMonth[month - 1];
}
}
/// Mode of the date picker dialog.
///
/// Either a calendar or text input. In [calendar] mode, a calendar view is
......@@ -47,8 +185,11 @@ enum DatePickerMode {
/// to specify allowable days in the date picker.
typedef SelectableDayPredicate = bool Function(DateTime day);
/// Encapsulates a start and end [DateTime] that represent the range of dates
/// between them.
/// Encapsulates a start and end [DateTime] that represent the range of dates.
///
/// The range includes the [start] and [end] dates. The [start] and [end] dates
/// may be equal to indicate a date range of a single day. The [start] date must
/// not be after the [end] date.
///
/// See also:
/// * [showDateRangePicker], which displays a dialog that allows the user to
......@@ -56,13 +197,12 @@ typedef SelectableDayPredicate = bool Function(DateTime day);
@immutable
class DateTimeRange {
/// Creates a date range for the given start and end [DateTime].
///
/// [start] and [end] must be non-null.
const DateTimeRange({
DateTimeRange({
required this.start,
required this.end,
}) : assert(start != null),
assert(end != null);
assert(end != null),
assert(!start.isAfter(end));
/// The start of the range of dates.
final DateTime start;
......
......@@ -9,15 +9,14 @@ 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';
import 'date.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';
// This is the original implementation for the Material Date Picker.
// These classes are deprecated and the whole file can be removed after
......
......@@ -5,14 +5,12 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../input_border.dart';
import '../input_decorator.dart';
import '../material_localizations.dart';
import '../text_form_field.dart';
import '../theme.dart';
import 'date_picker_common.dart';
import 'date_utils.dart' as utils;
import 'date.dart';
import 'input_border.dart';
import 'input_decorator.dart';
import 'material_localizations.dart';
import 'text_form_field.dart';
import 'theme.dart';
/// A [TextFormField] configured to accept and validate a date entered by a user.
///
......@@ -63,9 +61,9 @@ class InputDatePickerFormField extends StatefulWidget {
}) : assert(firstDate != null),
assert(lastDate != null),
assert(autofocus != null),
initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
initialDate = initialDate != null ? DateUtils.dateOnly(initialDate) : null,
firstDate = DateUtils.dateOnly(firstDate),
lastDate = DateUtils.dateOnly(lastDate),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
......@@ -156,6 +154,24 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateValueForSelectedDate();
}
@override
void didUpdateWidget(InputDatePickerFormField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialDate != oldWidget.initialDate) {
// Can't update the form field in the middle of a build, so do it next frame
WidgetsBinding.instance!.addPostFrameCallback((Duration timeStamp) {
setState(() {
_selectedDate = widget.initialDate;
_updateValueForSelectedDate();
});
});
}
}
void _updateValueForSelectedDate() {
if (_selectedDate != null) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
_inputText = localizations.formatCompactDate(_selectedDate!);
......@@ -169,6 +185,9 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
_autoSelected = true;
}
_controller.value = textEditingValue;
} else {
_inputText = '';
_controller.value = _controller.value.copyWith(text: _inputText);
}
}
......@@ -195,26 +214,21 @@ class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
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 _updateDate(String? text, ValueChanged<DateTime>? callback) {
final DateTime? date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
callback?.call(_selectedDate!);
}
}
void _handleSaved(String? text) {
_updateDate(text, widget.onDateSaved);
}
void _handleSubmitted(String text) {
if (widget.onDateSubmitted != null) {
final DateTime? date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSubmitted!(date!);
}
}
_updateDate(text, widget.onDateSubmitted);
}
@override
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import '../color_scheme.dart';
import '../icon_button.dart';
import '../material.dart';
import '../text_theme.dart';
import '../theme.dart';
// This is an internal implementation file. Even though there are public
// classes and functions defined here, they are only meant to be used by the
// date picker implementation and are not exported as part of the Material library.
// See pickers.dart for exactly what is considered part of the public API.
const double _datePickerHeaderLandscapeWidth = 152.0;
const double _datePickerHeaderPortraitHeight = 120.0;
const double _headerPaddingLandscape = 16.0;
/// Re-usable widget that displays the selected date (in large font) and the
/// help text above it.
///
/// These types include:
///
/// * Single Date picker with calendar mode.
/// * Single Date picker with manual input mode.
/// * Date Range picker with manual input mode.
///
/// [helpText], [orientation], [icon], [onIconPressed] are required and must be
/// non-null.
class DatePickerHeader extends StatelessWidget {
/// Creates a header for use in a date picker dialog.
const DatePickerHeader({
Key? key,
required this.helpText,
required this.titleText,
this.titleSemanticsLabel,
required this.titleStyle,
required this.orientation,
this.isShort = false,
required this.icon,
required this.iconTooltip,
required this.onIconPressed,
}) : assert(helpText != null),
assert(orientation != null),
assert(isShort != null),
super(key: key);
/// The text that is displayed at the top of the header.
///
/// This is used to indicate to the user what they are selecting a date for.
final String helpText;
/// The text that is displayed at the center of the header.
final String titleText;
/// The semantic label associated with the [titleText].
final String? titleSemanticsLabel;
/// The [TextStyle] that the title text is displayed with.
final TextStyle? titleStyle;
/// The orientation is used to decide how to layout its children.
final Orientation orientation;
/// Indicates the header is being displayed in a shorter/narrower context.
///
/// This will be used to tighten up the space between the help text and date
/// text if `true`. Additionally, it will use a smaller typography style if
/// `true`.
///
/// This is necessary for displaying the manual input mode in
/// landscape orientation, in order to account for the keyboard height.
final bool isShort;
/// The mode-switching icon that will be displayed in the lower right
/// in portrait, and lower left in landscape.
///
/// The available icons are described in [Icons].
final IconData icon;
/// The text that is displayed for the tooltip of the icon.
final String iconTooltip;
/// Callback when the user taps the icon in the header.
///
/// The picker will use this to toggle between entry modes.
final VoidCallback onIconPressed;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ColorScheme colorScheme = theme.colorScheme;
final TextTheme textTheme = theme.textTheme;
// The header should use the primary color in light themes and surface color in dark
final bool isDark = colorScheme.brightness == Brightness.dark;
final Color primarySurfaceColor = isDark ? colorScheme.surface : colorScheme.primary;
final Color onPrimarySurfaceColor = isDark ? colorScheme.onSurface : colorScheme.onPrimary;
final TextStyle? helpStyle = textTheme.overline?.copyWith(
color: onPrimarySurfaceColor,
);
final Text help = Text(
helpText,
style: helpStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
final Text title = Text(
titleText,
semanticsLabel: titleSemanticsLabel ?? titleText,
style: titleStyle,
maxLines: orientation == Orientation.portrait ? 1 : 2,
overflow: TextOverflow.ellipsis,
);
final IconButton icon = IconButton(
icon: Icon(this.icon),
color: onPrimarySurfaceColor,
tooltip: iconTooltip,
onPressed: onIconPressed,
);
switch (orientation) {
case Orientation.portrait:
return SizedBox(
height: _datePickerHeaderPortraitHeight,
child: Material(
color: primarySurfaceColor,
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: 24,
end: 12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 16),
help,
const Flexible(child: SizedBox(height: 38)),
Row(
children: <Widget>[
Expanded(child: title),
icon,
],
),
],
),
),
),
);
case Orientation.landscape:
return SizedBox(
width: _datePickerHeaderLandscapeWidth,
child: Material(
color: primarySurfaceColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: _headerPaddingLandscape,
),
child: help,
),
SizedBox(height: isShort ? 16 : 56),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: _headerPaddingLandscape,
),
child: title,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: icon,
),
],
),
),
);
}
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Common date utility functions used by the date picker implementation
// This is an internal implementation file. Even though there are public
// classes and functions defined here, they are only meant to be used by the
// date picker implementation and are not exported as part of the Material library.
// See pickers.dart for exactly what is considered part of the public API.
import '../material_localizations.dart';
import 'date_picker_common.dart';
/// Returns a [DateTime] with just the date of the original, but no time set.
DateTime dateOnly(DateTime date) {
return DateTime(date.year, date.month, date.day);
}
/// Returns a [DateTimeRange] with the dates of the original without any times set.
DateTimeRange datesOnly(DateTimeRange range) {
return DateTimeRange(start: dateOnly(range.start), end: dateOnly(range.end));
}
/// Returns true if the two [DateTime] objects have the same day, month, and
/// year, or are both null.
bool isSameDay(DateTime? dateA, DateTime? dateB) {
return
dateA?.year == dateB?.year &&
dateA?.month == dateB?.month &&
dateA?.day == dateB?.day;
}
/// Returns true if the two [DateTime] objects have the same month, and
/// year, or are both null.
bool isSameMonth(DateTime? dateA, DateTime? dateB) {
return
dateA?.year == dateB?.year &&
dateA?.month == dateB?.month;
}
/// Determines the number of months between two [DateTime] objects.
///
/// For example:
/// ```
/// DateTime date1 = DateTime(year: 2019, month: 6, day: 15);
/// DateTime date2 = DateTime(year: 2020, month: 1, day: 15);
/// int delta = monthDelta(date1, date2);
/// ```
///
/// The value for `delta` would be `7`.
int monthDelta(DateTime startDate, DateTime endDate) {
return (endDate.year - startDate.year) * 12 + endDate.month - startDate.month;
}
/// Returns a [DateTime] with the added number of months and truncates any day
/// and time information.
///
/// For example:
/// ```
/// DateTime date = DateTime(year: 2019, month: 1, day: 15);
/// DateTime futureDate = _addMonthsToMonthDate(date, 3);
/// ```
///
/// `date` would be January 15, 2019.
/// `futureDate` would be April 1, 2019 since it adds 3 months and truncates
/// any additional date information.
DateTime addMonthsToMonthDate(DateTime monthDate, int monthsToAdd) {
return DateTime(monthDate.year, monthDate.month + monthsToAdd);
}
/// Returns a [DateTime] with the added number of days and no time set.
DateTime addDaysToDate(DateTime date, int days) {
return DateTime(date.year, date.month, date.day + days);
}
/// Computes the offset from the first day of the week that the first day of
/// the [month] falls on.
///
/// For example, September 1, 2017 falls on a Friday, which in the calendar
/// localized for United States English appears as:
///
/// ```
/// S M T W T F S
/// _ _ _ _ _ 1 2
/// ```
///
/// The offset for the first day of the months is the number of leading blanks
/// in the calendar, i.e. 5.
///
/// The same date localized for the Russian calendar has a different offset,
/// because the first day of week is Monday rather than Sunday:
///
/// ```
/// M T W T F S S
/// _ _ _ _ 1 2 3
/// ```
///
/// So the offset is 4, rather than 5.
///
/// This code consolidates the following:
///
/// - [DateTime.weekday] provides a 1-based index into days of week, with 1
/// falling on Monday.
/// - [MaterialLocalizations.firstDayOfWeekIndex] provides a 0-based index
/// into the [MaterialLocalizations.narrowWeekdays] list.
/// - [MaterialLocalizations.narrowWeekdays] list provides localized names of
/// days of week, always starting with Sunday and ending with Saturday.
int firstDayOffset(int year, int month, MaterialLocalizations localizations) {
// 0-based day of week for the month and year, with 0 representing Monday.
final int weekdayFromMonday = DateTime(year, month).weekday - 1;
// 0-based start of week depending on the locale, with 0 representing Sunday.
int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex;
// firstDayOfWeekIndex recomputed to be Monday-based, in order to compare with
// weekdayFromMonday.
firstDayOfWeekIndex = (firstDayOfWeekIndex - 1) % 7;
// Number of days between the first day of week appearing on the calendar,
// and the day corresponding to the first of the month.
return (weekdayFromMonday - firstDayOfWeekIndex) % 7;
}
/// Returns the number of days in a month, according to the proleptic
/// Gregorian calendar.
///
/// This applies the leap year logic introduced by the Gregorian reforms of
/// 1582. It will not give valid results for dates prior to that time.
int getDaysInMonth(int year, int month) {
if (month == DateTime.february) {
final bool isLeapYear = (year % 4 == 0) && (year % 100 != 0) ||
(year % 400 == 0);
if (isLeapYear)
return 29;
return 28;
}
const List<int> daysInMonth = <int>[31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return daysInMonth[month - 1];
}
/// Returns a locale-appropriate string to describe the start of a date range.
///
/// If `startDate` is null, then it defaults to 'Start Date', otherwise if it
/// is in the same year as the `endDate` then it will use the short month
/// day format (i.e. 'Jan 21'). Otherwise it will return the short date format
/// (i.e. 'Jan 21, 2020').
String formatRangeStartDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate) {
return startDate == null
? localizations.dateRangeStartLabel
: (endDate == null || startDate.year == endDate.year)
? localizations.formatShortMonthDay(startDate)
: localizations.formatShortDate(startDate);
}
/// Returns an locale-appropriate string to describe the end of a date range.
///
/// If `endDate` is null, then it defaults to 'End Date', otherwise if it
/// is in the same year as the `startDate` and the `currentDate` then it will
/// just use the short month day format (i.e. 'Jan 21'), otherwise it will
/// include the year (i.e. 'Jan 21, 2020').
String formatRangeEndDate(MaterialLocalizations localizations, DateTime? startDate, DateTime? endDate, DateTime currentDate) {
return endDate == null
? localizations.dateRangeEndLabel
: (startDate != null && startDate.year == endDate.year && startDate.year == currentDate.year)
? localizations.formatShortMonthDay(endDate)
: localizations.formatShortDate(endDate);
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../input_border.dart';
import '../input_decorator.dart';
import '../material_localizations.dart';
import '../text_field.dart';
import '../theme.dart';
import 'date_utils.dart' as utils;
/// Provides a pair of text fields that allow the user to enter the start and
/// end dates that represent a range of dates.
//
// This is not publicly exported (see pickers.dart), as it is just an
// internal component used by [showDateRangePicker].
class InputDateRangePicker extends StatefulWidget {
/// Creates a row with two text fields configured to accept the start and end dates
/// of a date range.
InputDateRangePicker({
Key? key,
DateTime? initialStartDate,
DateTime? initialEndDate,
required DateTime firstDate,
required DateTime lastDate,
required this.onStartDateChanged,
required this.onEndDateChanged,
this.helpText,
this.errorFormatText,
this.errorInvalidText,
this.errorInvalidRangeText,
this.fieldStartHintText,
this.fieldEndHintText,
this.fieldStartLabelText,
this.fieldEndLabelText,
this.autofocus = false,
this.autovalidate = false,
}) : initialStartDate = initialStartDate == null ? null : utils.dateOnly(initialStartDate),
initialEndDate = initialEndDate == null ? null : utils.dateOnly(initialEndDate),
assert(firstDate != null),
firstDate = utils.dateOnly(firstDate),
assert(lastDate != null),
lastDate = utils.dateOnly(lastDate),
assert(firstDate != null),
assert(lastDate != null),
assert(autofocus != null),
assert(autovalidate != null),
super(key: key);
/// The [DateTime] that represents the start of the initial date range selection.
final DateTime? initialStartDate;
/// The [DateTime] that represents the end of the initial date range selection.
final DateTime? initialEndDate;
/// The earliest allowable [DateTime] that the user can select.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can select.
final DateTime lastDate;
/// Called when the user changes the start date of the selected range.
final ValueChanged<DateTime?>? onStartDateChanged;
/// Called when the user changes the end date of the selected range.
final ValueChanged<DateTime?>? onEndDateChanged;
/// The text that is displayed at the top of the header.
///
/// This is used to indicate to the user what they are selecting a date for.
final String? helpText;
/// Error text used to indicate the text in a field is not a valid date.
final String? errorFormatText;
/// Error text used to indicate the date in a field is not in the valid range
/// of [firstDate] - [lastDate].
final String? errorInvalidText;
/// Error text used to indicate the dates given don't form a valid date
/// range (i.e. the start date is after the end date).
final String? errorInvalidRangeText;
/// Hint text shown when the start date field is empty.
final String? fieldStartHintText;
/// Hint text shown when the end date field is empty.
final String? fieldEndHintText;
/// Label used for the start date field.
final String? fieldStartLabelText;
/// Label used for the end date field.
final String? fieldEndLabelText;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
/// If true, this the date fields will validate and update their error text
/// immediately after every change. Otherwise, you must call
/// [InputDateRangePickerState.validate] to validate.
final bool autovalidate;
@override
InputDateRangePickerState createState() => InputDateRangePickerState();
}
/// The current state of an [InputDateRangePicker]. Can be used to
/// [validate] the date field entries.
class InputDateRangePickerState extends State<InputDateRangePicker> {
late String _startInputText;
late String _endInputText;
DateTime? _startDate;
DateTime? _endDate;
late TextEditingController _startController;
late TextEditingController _endController;
String? _startErrorText;
String? _endErrorText;
bool _autoSelected = false;
@override
void initState() {
super.initState();
_startDate = widget.initialStartDate;
_startController = TextEditingController();
_endDate = widget.initialEndDate;
_endController = TextEditingController();
}
@override
void dispose() {
_startController.dispose();
_endController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (_startDate != null) {
_startInputText = localizations.formatCompactDate(_startDate!);
final bool selectText = widget.autofocus && !_autoSelected;
_updateController(_startController, _startInputText, selectText);
_autoSelected = selectText;
}
if (_endDate != null) {
_endInputText = localizations.formatCompactDate(_endDate!);
_updateController(_endController, _endInputText, false);
}
}
/// Validates that the text in the start and end fields represent a valid
/// date range.
///
/// Will return true if the range is valid. If not, it will
/// return false and display an appropriate error message under one of the
/// text fields.
bool validate() {
String? startError = _validateDate(_startDate);
final String? endError = _validateDate(_endDate);
if (startError == null && endError == null) {
if (_startDate!.isAfter(_endDate!)) {
startError = widget.errorInvalidRangeText ?? MaterialLocalizations.of(context).invalidDateRangeLabel;
}
}
setState(() {
_startErrorText = startError;
_endErrorText = endError;
});
return startError == null && endError == null;
}
DateTime? _parseDate(String? text) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.parseCompactDate(text);
}
String? _validateDate(DateTime? date) {
if (date == null) {
return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel;
} else if (date.isBefore(widget.firstDate) || date.isAfter(widget.lastDate)) {
return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel;
}
return null;
}
void _updateController(TextEditingController controller, String text, bool selectText) {
TextEditingValue textEditingValue = controller.value.copyWith(text: text);
if (selectText) {
textEditingValue = textEditingValue.copyWith(selection: TextSelection(
baseOffset: 0,
extentOffset: text.length,
));
}
controller.value = textEditingValue;
}
void _handleStartChanged(String text) {
setState(() {
_startInputText = text;
_startDate = _parseDate(text);
widget.onStartDateChanged?.call(_startDate);
});
if (widget.autovalidate) {
validate();
}
}
void _handleEndChanged(String text) {
setState(() {
_endInputText = text;
_endDate = _parseDate(text);
widget.onEndDateChanged?.call(_endDate);
});
if (widget.autovalidate) {
validate();
}
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(
child: TextField(
controller: _startController,
decoration: InputDecoration(
border: inputTheme.border ?? const UnderlineInputBorder(),
filled: inputTheme.filled,
hintText: widget.fieldStartHintText ?? localizations.dateHelpText,
labelText: widget.fieldStartLabelText ?? localizations.dateRangeStartLabel,
errorText: _startErrorText,
),
keyboardType: TextInputType.datetime,
onChanged: _handleStartChanged,
autofocus: widget.autofocus,
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _endController,
decoration: InputDecoration(
border: inputTheme.border ?? const UnderlineInputBorder(),
filled: inputTheme.filled,
hintText: widget.fieldEndHintText ?? localizations.dateHelpText,
labelText: widget.fieldEndLabelText ?? localizations.dateRangeEndLabel,
errorText: _endErrorText,
),
keyboardType: TextInputType.datetime,
onChanged: _handleEndChanged,
),
),
],
);
}
}
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Date Picker public API
export 'calendar_date_picker.dart' show CalendarDatePicker;
export 'date_picker_common.dart' show
DatePickerEntryMode,
DatePickerMode,
DateTimeRange,
SelectableDayPredicate;
export 'date_picker_deprecated.dart';
export 'date_picker_dialog.dart' show showDatePicker;
export 'date_range_picker_dialog.dart' show showDateRangePicker;
export 'input_date_picker.dart' show InputDatePickerFormField;
// TODO(ianh): Not exporting everything is unusual and we should
// probably change to just exporting everything and making sure it's
// acceptable as a public API, or, worst case, merging the parts
// that really must be public into a single file and make them
// actually private.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment