mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Cupertino Date Picker (#21251)
This commit is contained in:
parent
efc5123d29
commit
2a8e35cc9c
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../gallery/demo.dart';
|
||||
import 'cupertino_navigation_demo.dart' show coolColorNames;
|
||||
@ -22,6 +23,15 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
|
||||
Duration timer = Duration();
|
||||
|
||||
// Value that is shown in the date picker in date mode.
|
||||
DateTime date = DateTime.now();
|
||||
|
||||
// Value that is shown in the date picker in time mode.
|
||||
DateTime time = DateTime.now();
|
||||
|
||||
// Value that is shown in the date picker in dateAndTime mode.
|
||||
DateTime dateTime = DateTime.now();
|
||||
|
||||
Widget _buildMenu(List<Widget> children) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
@ -53,30 +63,10 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorPicker() {
|
||||
final FixedExtentScrollController scrollController =
|
||||
FixedExtentScrollController(initialItem: _selectedColorIndex);
|
||||
return CupertinoPicker(
|
||||
scrollController: scrollController,
|
||||
itemExtent: _kPickerItemHeight,
|
||||
backgroundColor: CupertinoColors.white,
|
||||
onSelectedItemChanged: (int index) {
|
||||
setState(() {
|
||||
_selectedColorIndex = index;
|
||||
});
|
||||
},
|
||||
children: List<Widget>.generate(coolColorNames.length, (int index) {
|
||||
return Center(child:
|
||||
Text(coolColorNames[index]),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomPicker(Widget picker) {
|
||||
return Container(
|
||||
height: _kPickerSheetHeight,
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
color: CupertinoColors.white,
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
@ -95,6 +85,47 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorPicker(BuildContext context) {
|
||||
final FixedExtentScrollController scrollController =
|
||||
FixedExtentScrollController(initialItem: _selectedColorIndex);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _buildBottomPicker(
|
||||
CupertinoPicker(
|
||||
scrollController: scrollController,
|
||||
itemExtent: _kPickerItemHeight,
|
||||
backgroundColor: CupertinoColors.white,
|
||||
onSelectedItemChanged: (int index) {
|
||||
setState(() => _selectedColorIndex = index);
|
||||
},
|
||||
children: List<Widget>.generate(coolColorNames.length, (int index) {
|
||||
return Center(child:
|
||||
Text(coolColorNames[index]),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Favorite Color'),
|
||||
Text(
|
||||
coolColorNames[_selectedColorIndex],
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.inactiveGray
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountdownTimerPicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@ -105,9 +136,7 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
CupertinoTimerPicker(
|
||||
initialTimerDuration: timer,
|
||||
onTimerDurationChanged: (Duration newTimer) {
|
||||
setState(() {
|
||||
timer = newTimer;
|
||||
});
|
||||
setState(() => timer = newTimer);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -115,15 +144,105 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Countdown Timer'),
|
||||
Text(
|
||||
'${timer.inHours}:'
|
||||
<Widget>[
|
||||
const Text('Countdown Timer'),
|
||||
Text(
|
||||
'${timer.inHours}:'
|
||||
'${(timer.inMinutes % 60).toString().padLeft(2,'0')}:'
|
||||
'${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
|
||||
style: const TextStyle(color: CupertinoColors.inactiveGray),
|
||||
),
|
||||
]
|
||||
style: const TextStyle(color: CupertinoColors.inactiveGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDatePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _buildBottomPicker(
|
||||
CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
initialDateTime: date,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => date = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Date'),
|
||||
Text(
|
||||
DateFormat.yMMMMd().format(date),
|
||||
style: const TextStyle(color: CupertinoColors.inactiveGray),
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _buildBottomPicker(
|
||||
CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
initialDateTime: time,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => time = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Time'),
|
||||
Text(
|
||||
DateFormat.jm().format(time),
|
||||
style: const TextStyle(color: CupertinoColors.inactiveGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateAndTimePicker(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _buildBottomPicker(
|
||||
CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
initialDateTime: dateTime,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => dateTime = newDateTime);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Date and Time'),
|
||||
Text(
|
||||
DateFormat.yMMMd().add_jm().format(dateTime),
|
||||
style: const TextStyle(color: CupertinoColors.inactiveGray),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -146,28 +265,11 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
const Padding(padding: EdgeInsets.only(top: 32.0)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return _buildBottomPicker(_buildColorPicker());
|
||||
},
|
||||
);
|
||||
},
|
||||
child: _buildMenu(
|
||||
<Widget>[
|
||||
const Text('Favorite Color'),
|
||||
Text(
|
||||
coolColorNames[_selectedColorIndex],
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.inactiveGray
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
_buildColorPicker(context),
|
||||
_buildCountdownTimerPicker(context),
|
||||
_buildDatePicker(context),
|
||||
_buildTimePicker(context),
|
||||
_buildDateAndTimePicker(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -2,30 +2,906 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'localizations.dart';
|
||||
import 'picker.dart';
|
||||
|
||||
/// Default aesthetic values obtained by comparing with iOS pickers.
|
||||
// Default aesthetic values obtained by comparing with iOS pickers.
|
||||
const double _kItemExtent = 32.0;
|
||||
const double _kPickerWidth = 330.0;
|
||||
/// Considers setting the default background color from the theme, in the future.
|
||||
const bool _kUseMagnifier = true;
|
||||
const double _kMagnification = 1.1;
|
||||
const double _kDatePickerPadSize = 12.0;
|
||||
// Considers setting the default background color from the theme, in the future.
|
||||
const Color _kBackgroundColor = CupertinoColors.white;
|
||||
|
||||
// Lays out the date picker based on how much space each single column needs.
|
||||
//
|
||||
// Each column is a child of this delegate, indexed from 0 to number of columns - 1.
|
||||
// Each column will be padded horizontally by 12.0 both left and right.
|
||||
//
|
||||
// The picker will be placed in the center, and the leftmost and rightmost
|
||||
// column will be extended equally to the remaining width.
|
||||
class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
|
||||
_DatePickerLayoutDelegate({
|
||||
@required this.columnWidths,
|
||||
@required this.textDirectionFactor,
|
||||
}) : assert(columnWidths != null),
|
||||
assert(textDirectionFactor != null);
|
||||
|
||||
// The iOS timer picker has its width fixed to 330.0 in all modes.
|
||||
// The list containing widths of all columns.
|
||||
final List<double> columnWidths;
|
||||
|
||||
// textDirectionFactor is 1 if text is written left to right, and -1 if right to left.
|
||||
final int textDirectionFactor;
|
||||
|
||||
@override
|
||||
void performLayout(Size size) {
|
||||
double remainingWidth = size.width;
|
||||
|
||||
for (int i = 0; i < columnWidths.length; i++)
|
||||
remainingWidth -= columnWidths[i] + _kDatePickerPadSize * 2;
|
||||
|
||||
double currentHorizontalOffset = 0.0;
|
||||
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
final int index = textDirectionFactor == 1 ? i : columnWidths.length - i - 1;
|
||||
|
||||
double childWidth = columnWidths[index] + _kDatePickerPadSize * 2;
|
||||
if (index == 0 || index == columnWidths.length - 1)
|
||||
childWidth += remainingWidth / 2;
|
||||
|
||||
layoutChild(index, BoxConstraints.tight(Size(childWidth, size.height)));
|
||||
positionChild(index, Offset(currentHorizontalOffset, 0.0));
|
||||
|
||||
currentHorizontalOffset += childWidth;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_DatePickerLayoutDelegate oldDelegate) {
|
||||
return columnWidths != oldDelegate.columnWidths
|
||||
|| textDirectionFactor != oldDelegate.textDirectionFactor;
|
||||
}
|
||||
}
|
||||
|
||||
/// Different display modes of [CupertinoDatePicker].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoDatePicker], the class that implements different display modes
|
||||
/// of the iOS-style date picker.
|
||||
/// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
|
||||
enum CupertinoDatePickerMode {
|
||||
/// Mode that shows the date in hour, minute, and (optional) an AM/PM designation.
|
||||
/// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
|
||||
/// Column order is subject to internationalization.
|
||||
///
|
||||
/// Example: [4 | 14 | PM].
|
||||
time,
|
||||
/// Mode that shows the date in month, day of month, and year.
|
||||
/// Name of month is spelled in full.
|
||||
/// Column order is subject to internationalization.
|
||||
///
|
||||
/// Example: [July | 13 | 2012].
|
||||
date,
|
||||
/// Mode that shows the date as day of the week, month, day of month and
|
||||
/// the time in hour, minute, and (optional) an AM/PM designation.
|
||||
/// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
|
||||
/// Column order is subject to internationalization.
|
||||
///
|
||||
/// Example: [Fri Jul 13 | 4 | 14 | PM]
|
||||
dateAndTime,
|
||||
}
|
||||
|
||||
// Different types of column in CupertinoDatePicker.
|
||||
enum _PickerColumnType {
|
||||
// Day of month column in date mode.
|
||||
dayOfMonth,
|
||||
// Month column in date mode.
|
||||
month,
|
||||
// Year column in date mode.
|
||||
year,
|
||||
// Medium date column in dateAndTime mode.
|
||||
date,
|
||||
// Hour column in time and dateAndTime mode.
|
||||
hour,
|
||||
// minute column in time and dateAndTime mode.
|
||||
minute,
|
||||
// AM/PM column in time and dateAndTime mode.
|
||||
dayPeriod,
|
||||
}
|
||||
|
||||
/// A date picker widget in iOS style.
|
||||
///
|
||||
/// There are several modes of the date picker listed in [CupertinoDatePickerMode].
|
||||
///
|
||||
/// The class will display its children as consecutive columns. Its children
|
||||
/// order is based on internationalization.
|
||||
///
|
||||
/// Example of the picker in date mode:
|
||||
///
|
||||
/// * US-English: [July | 13 | 2012]
|
||||
/// * Vietnamese: [13 | Tháng 7 | 2012]
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
|
||||
/// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
|
||||
class CupertinoDatePicker extends StatefulWidget {
|
||||
/// Constructs an iOS style date picker.
|
||||
///
|
||||
/// [mode] is one of the mode listed in [CupertinoDatePickerMode] and defaults
|
||||
/// to [CupertinoDatePickerMode.dateAndTime].
|
||||
///
|
||||
/// [onDateTimeChanged] is the callback called when the selected date or time
|
||||
/// changes and must not be null.
|
||||
///
|
||||
/// [initialDateTime] is the initial date time of the picker. Defaults to the
|
||||
/// present date and time and must not be null. The present must conform to
|
||||
/// the intervals set in [minimumDate], [maximumDate], [minimumYear], and
|
||||
/// [maximumYear].
|
||||
///
|
||||
/// [minimumDate] is the minimum date that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
|
||||
///
|
||||
/// [maximumDate] is the maximum date that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
|
||||
///
|
||||
/// [minimumYear] is the minimum year that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
|
||||
///
|
||||
/// [maximumYear] is the maximum year that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
|
||||
///
|
||||
/// [minuteInterval] is the granularity of the minute spinner. Must be a
|
||||
/// positive integer factor of 60.
|
||||
///
|
||||
/// [use24hFormat] decides whether 24 hour format is used. Defaults to false.
|
||||
CupertinoDatePicker({
|
||||
this.mode = CupertinoDatePickerMode.dateAndTime,
|
||||
@required this.onDateTimeChanged,
|
||||
// ignore: always_require_non_null_named_parameters
|
||||
DateTime initialDateTime,
|
||||
this.minimumDate,
|
||||
this.maximumDate,
|
||||
this.minimumYear = 1,
|
||||
this.maximumYear,
|
||||
this.minuteInterval = 1,
|
||||
this.use24hFormat = false,
|
||||
}) : this.initialDateTime = initialDateTime ?? DateTime.now(),
|
||||
assert(mode != null),
|
||||
assert(onDateTimeChanged != null),
|
||||
assert(initialDateTime != null),
|
||||
assert(
|
||||
mode != CupertinoDatePickerMode.dateAndTime || minimumDate == null || !initialDateTime.isBefore(minimumDate),
|
||||
'initial date is before minimum date',
|
||||
),
|
||||
assert(
|
||||
mode != CupertinoDatePickerMode.dateAndTime || maximumDate == null || !initialDateTime.isAfter(maximumDate),
|
||||
'initial date is after maximum date',
|
||||
),
|
||||
assert(minimumYear != null),
|
||||
assert(
|
||||
mode != CupertinoDatePickerMode.date || (minimumYear >= 1 && initialDateTime.year >= minimumYear),
|
||||
'initial year is not greater than minimum year, or mininum year is not positive',
|
||||
),
|
||||
assert(
|
||||
mode != CupertinoDatePickerMode.date || maximumYear == null || initialDateTime.year <= maximumYear,
|
||||
'initial year is not smaller than maximum year',
|
||||
),
|
||||
assert(
|
||||
minuteInterval > 0 && 60 % minuteInterval == 0,
|
||||
'minute interval is not a positive integer factor of 60',
|
||||
),
|
||||
assert(
|
||||
initialDateTime.minute % minuteInterval == 0,
|
||||
'initial minute is not divisible by minute interval',
|
||||
);
|
||||
|
||||
/// The mode of the date picker as one of [CupertinoDatePickerMode].
|
||||
/// Defaults to [CupertinoDatePickerMode.dateAndTime]. Cannot be null and
|
||||
/// value cannot change after initial build.
|
||||
final CupertinoDatePickerMode mode;
|
||||
|
||||
/// The initial date and/or time of the picker. Defaults to the present date
|
||||
/// and time and must not be null. The present must conform to the intervals
|
||||
/// set in [minimumDate], [maximumDate], [minimumYear], and [maximumYear].
|
||||
///
|
||||
/// Changing this value after the initial build will not affect the currently
|
||||
/// selected date time.
|
||||
final DateTime initialDateTime;
|
||||
|
||||
/// Minimum date that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
|
||||
final DateTime minimumDate;
|
||||
|
||||
/// Maximum date that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.dateAndTime] mode. Null if there's no limit.
|
||||
final DateTime maximumDate;
|
||||
|
||||
/// Minimum year that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
|
||||
final int minimumYear;
|
||||
|
||||
/// Maximum year that the picker can be scrolled to in
|
||||
/// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
|
||||
final int maximumYear;
|
||||
|
||||
/// The granularity of the minutes spinner, if it is shown in the current mode.
|
||||
/// Must be an integer factor of 60.
|
||||
final int minuteInterval;
|
||||
|
||||
/// Whether to use 24 hour format. Defaults to false.
|
||||
final bool use24hFormat;
|
||||
|
||||
/// Callback called when the selected date and/or time changes. Must not be
|
||||
/// null.
|
||||
final ValueChanged<DateTime> onDateTimeChanged;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
// The `time` mode and `dateAndTime` mode of the picker share the time
|
||||
// columns, so they are placed together to one state.
|
||||
// The `date` mode has different children and is implemented in a different
|
||||
// state.
|
||||
if (mode == CupertinoDatePickerMode.time || mode == CupertinoDatePickerMode.dateAndTime)
|
||||
return _CupertinoDatePickerDateTimeState();
|
||||
else
|
||||
return _CupertinoDatePickerDateState();
|
||||
}
|
||||
|
||||
// Estimate the minimum width that each column needs to layout its content.
|
||||
static double _getColumnWidth(
|
||||
_PickerColumnType columnType,
|
||||
CupertinoLocalizations localizations,
|
||||
BuildContext context,
|
||||
) {
|
||||
String longestText = '';
|
||||
|
||||
switch (columnType) {
|
||||
case _PickerColumnType.date:
|
||||
// Measuring the length of all possible date is impossible, so here
|
||||
// just some dates are measured.
|
||||
for (int i = 1; i <= 12; i++) {
|
||||
// An arbitrary date.
|
||||
final String date =
|
||||
localizations.datePickerMediumDate(DateTime(2018, i, 25));
|
||||
if (longestText.length < date.length)
|
||||
longestText = date;
|
||||
}
|
||||
break;
|
||||
case _PickerColumnType.hour:
|
||||
for (int i = 0 ; i < 24; i++) {
|
||||
final String hour = localizations.datePickerHour(i);
|
||||
if (longestText.length < hour.length)
|
||||
longestText = hour;
|
||||
}
|
||||
break;
|
||||
case _PickerColumnType.minute:
|
||||
for (int i = 0 ; i < 60; i++) {
|
||||
final String minute = localizations.datePickerMinute(i);
|
||||
if (longestText.length < minute.length)
|
||||
longestText = minute;
|
||||
}
|
||||
break;
|
||||
case _PickerColumnType.dayPeriod:
|
||||
longestText =
|
||||
localizations.anteMeridiemAbbreviation.length > localizations.postMeridiemAbbreviation.length
|
||||
? localizations.anteMeridiemAbbreviation
|
||||
: localizations.postMeridiemAbbreviation;
|
||||
break;
|
||||
case _PickerColumnType.dayOfMonth:
|
||||
for (int i = 1 ; i <=31; i++) {
|
||||
final String dayOfMonth = localizations.datePickerDayOfMonth(i);
|
||||
if (longestText.length < dayOfMonth.length)
|
||||
longestText = dayOfMonth;
|
||||
}
|
||||
break;
|
||||
case _PickerColumnType.month:
|
||||
for (int i = 1 ; i <=12; i++) {
|
||||
final String month = localizations.datePickerMonth(i);
|
||||
if (longestText.length < month.length)
|
||||
longestText = month;
|
||||
}
|
||||
break;
|
||||
case _PickerColumnType.year:
|
||||
longestText = localizations.datePickerYear(2018);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(longestText != '', 'column type is not appropriate');
|
||||
|
||||
final TextPainter painter = TextPainter(
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
text: longestText,
|
||||
),
|
||||
textDirection: Directionality.of(context),
|
||||
);
|
||||
|
||||
// This operation is expensive and should be avoided. It is called here only
|
||||
// because there's no other way to get the information we want without
|
||||
// laying out the text.
|
||||
painter.layout();
|
||||
|
||||
return painter.maxIntrinsicWidth;
|
||||
}
|
||||
}
|
||||
|
||||
typedef _ColumnBuilder = Widget Function(double offAxisFraction, TransitionBuilder itemPositioningBuilder);
|
||||
|
||||
class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
|
||||
int textDirectionFactor;
|
||||
CupertinoLocalizations localizations;
|
||||
|
||||
// Alignment based on text direction. The variable name is self descriptive,
|
||||
// however, when text direction is rtl, alignment is reversed.
|
||||
Alignment alignCenterLeft;
|
||||
Alignment alignCenterRight;
|
||||
|
||||
// Read this out when the state is initially created. Changes in initialDateTime
|
||||
// in the widget after first build is ignored.
|
||||
DateTime initialDateTime;
|
||||
|
||||
// The currently selected values of the date picker.
|
||||
int selectedDayFromInitial; // The difference in days between the initial date and the currently selected date.
|
||||
int selectedHour;
|
||||
int selectedMinute;
|
||||
int selectedAmPm; // 0 means AM, 1 means PM.
|
||||
|
||||
// The controller of the AM/PM column.
|
||||
FixedExtentScrollController amPmController;
|
||||
|
||||
// Estimated width of columns.
|
||||
final Map<int, double> estimatedColumnWidths = <int, double>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initialDateTime = widget.initialDateTime;
|
||||
selectedDayFromInitial = 0;
|
||||
selectedHour = widget.initialDateTime.hour;
|
||||
selectedMinute = widget.initialDateTime.minute;
|
||||
selectedAmPm = 0;
|
||||
|
||||
if (!widget.use24hFormat) {
|
||||
selectedAmPm = selectedHour ~/ 12;
|
||||
selectedHour = selectedHour % 12;
|
||||
if (selectedHour == 0)
|
||||
selectedHour = 12;
|
||||
|
||||
amPmController = FixedExtentScrollController(initialItem: selectedAmPm);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoDatePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
assert(
|
||||
oldWidget.mode == widget.mode,
|
||||
"The CupertinoDatePicker's mode cannot change once it's built",
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
|
||||
localizations = CupertinoLocalizations.of(context) ?? const DefaultCupertinoLocalizations();
|
||||
|
||||
alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
|
||||
alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
|
||||
|
||||
estimatedColumnWidths.clear();
|
||||
}
|
||||
|
||||
// Lazily calculate the column width of the column being displayed only.
|
||||
double _getEstimatedColumnWidth(_PickerColumnType columnType) {
|
||||
if (estimatedColumnWidths[columnType.index] == null) {
|
||||
estimatedColumnWidths[columnType.index] =
|
||||
CupertinoDatePicker._getColumnWidth(columnType, localizations, context);
|
||||
}
|
||||
|
||||
return estimatedColumnWidths[columnType.index];
|
||||
}
|
||||
|
||||
// Gets the current date time of the picker.
|
||||
DateTime _getDateTime() {
|
||||
final DateTime date = DateTime(
|
||||
initialDateTime.year,
|
||||
initialDateTime.month,
|
||||
initialDateTime.day,
|
||||
).add(Duration(days: selectedDayFromInitial));
|
||||
|
||||
return DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
selectedHour + selectedAmPm * 12,
|
||||
selectedMinute,
|
||||
);
|
||||
}
|
||||
|
||||
// Builds the date column. The date is displayed in medium date format (e.g. Fri Aug 31).
|
||||
Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker.builder(
|
||||
scrollController: FixedExtentScrollController(initialItem: selectedDayFromInitial),
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedDayFromInitial = index;
|
||||
widget.onDateTimeChanged(_getDateTime());
|
||||
},
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final DateTime dateTime = DateTime(
|
||||
initialDateTime.year,
|
||||
initialDateTime.month,
|
||||
initialDateTime.day,
|
||||
).add(Duration(days: index));
|
||||
|
||||
if (widget.minimumDate != null && dateTime.isBefore(widget.minimumDate))
|
||||
return null;
|
||||
if (widget.maximumDate != null && dateTime.isAfter(widget.maximumDate))
|
||||
return null;
|
||||
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(localizations.datePickerMediumDate(dateTime)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker(
|
||||
scrollController: FixedExtentScrollController(initialItem: selectedHour),
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
if (widget.use24hFormat) {
|
||||
selectedHour = index;
|
||||
widget.onDateTimeChanged(_getDateTime());
|
||||
}
|
||||
else {
|
||||
final int currentHourIn24h = selectedHour + selectedAmPm * 12;
|
||||
// Automatically scrolls the am/pm column when the hour column value
|
||||
// goes far enough. This behavior is similar to
|
||||
// iOS picker version.
|
||||
if (currentHourIn24h ~/ 12 != index ~/ 12) {
|
||||
selectedHour = index % 12;
|
||||
amPmController.animateToItem(
|
||||
1 - amPmController.selectedItem,
|
||||
duration: const Duration(milliseconds: 300), // Set by comparing with iOS version.
|
||||
curve: Curves.easeOut,
|
||||
); // Set by comparing with iOS version.
|
||||
}
|
||||
else {
|
||||
selectedHour = index % 12;
|
||||
widget.onDateTimeChanged(_getDateTime());
|
||||
}
|
||||
}
|
||||
},
|
||||
children: List<Widget>.generate(24, (int index) {
|
||||
int hour = index;
|
||||
if (!widget.use24hFormat)
|
||||
hour = hour % 12 == 0 ? 12 : hour % 12;
|
||||
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(
|
||||
localizations.datePickerHour(hour),
|
||||
semanticsLabel: localizations.datePickerHourSemanticsLabel(hour),
|
||||
),
|
||||
);
|
||||
}),
|
||||
looping: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker(
|
||||
scrollController: FixedExtentScrollController(initialItem: selectedMinute),
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedMinute = index * widget.minuteInterval;
|
||||
widget.onDateTimeChanged(_getDateTime());
|
||||
},
|
||||
children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) {
|
||||
final int minute = index * widget.minuteInterval;
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(
|
||||
localizations.datePickerMinute(minute),
|
||||
semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute),
|
||||
),
|
||||
);
|
||||
}),
|
||||
looping: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker(
|
||||
scrollController: amPmController,
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedAmPm = index;
|
||||
widget.onDateTimeChanged(_getDateTime());
|
||||
},
|
||||
children: List<Widget>.generate(2, (int index) {
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(
|
||||
index == 0
|
||||
? localizations.anteMeridiemAbbreviation
|
||||
: localizations.postMeridiemAbbreviation
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Widths of the columns in this picker, ordered from left to right.
|
||||
final List<double> columnWidths = <double>[
|
||||
_getEstimatedColumnWidth(_PickerColumnType.hour),
|
||||
_getEstimatedColumnWidth(_PickerColumnType.minute),
|
||||
];
|
||||
final List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[
|
||||
_buildHourPicker,
|
||||
_buildMinutePicker,
|
||||
];
|
||||
|
||||
// Adds am/pm column if the picker is not using 24h format.
|
||||
if (!widget.use24hFormat) {
|
||||
if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.date_time_dayPeriod
|
||||
|| localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date) {
|
||||
pickerBuilders.add(_buildAmPmPicker);
|
||||
columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
|
||||
}
|
||||
else {
|
||||
pickerBuilders.insert(0, _buildAmPmPicker);
|
||||
columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
|
||||
}
|
||||
}
|
||||
|
||||
// Adds medium date column if the picker's mode is date and time.
|
||||
if (widget.mode == CupertinoDatePickerMode.dateAndTime) {
|
||||
if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date
|
||||
|| localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.dayPeriod_time_date) {
|
||||
pickerBuilders.add(_buildMediumDatePicker);
|
||||
columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.date));
|
||||
}
|
||||
else {
|
||||
pickerBuilders.insert(0, _buildMediumDatePicker);
|
||||
columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.date));
|
||||
}
|
||||
}
|
||||
|
||||
final List<Widget> pickers = <Widget>[];
|
||||
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
double offAxisFraction = 0.0;
|
||||
if (i == 0)
|
||||
offAxisFraction = -0.5 * textDirectionFactor;
|
||||
else if (i >= 2 || columnWidths.length == 2)
|
||||
offAxisFraction = 0.5 * textDirectionFactor;
|
||||
|
||||
EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
|
||||
if (i == columnWidths.length - 1)
|
||||
padding = padding.flipped;
|
||||
if (textDirectionFactor == -1)
|
||||
padding = padding.flipped;
|
||||
|
||||
pickers.add(LayoutId(
|
||||
id: i,
|
||||
child: pickerBuilders[i](
|
||||
offAxisFraction,
|
||||
(BuildContext context, Widget child) {
|
||||
return Container(
|
||||
alignment: i == columnWidths.length - 1
|
||||
? alignCenterLeft
|
||||
: alignCenterRight,
|
||||
padding: padding,
|
||||
child: Container(
|
||||
alignment: i == columnWidths.length - 1 ? alignCenterLeft : alignCenterRight,
|
||||
width: i == 0 || i == columnWidths.length - 1
|
||||
? null
|
||||
: columnWidths[i] + _kDatePickerPadSize,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return MediaQuery(
|
||||
data: const MediaQueryData(textScaleFactor: 1.0),
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _DatePickerLayoutDelegate(
|
||||
columnWidths: columnWidths,
|
||||
textDirectionFactor: textDirectionFactor,
|
||||
),
|
||||
children: pickers,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
|
||||
int textDirectionFactor;
|
||||
CupertinoLocalizations localizations;
|
||||
|
||||
// Alignment based on text direction. The variable name is self descriptive,
|
||||
// however, when text direction is rtl, alignment is reversed.
|
||||
Alignment alignCenterLeft;
|
||||
Alignment alignCenterRight;
|
||||
|
||||
// The currently selected values of the picker.
|
||||
int selectedDay;
|
||||
int selectedMonth;
|
||||
int selectedYear;
|
||||
|
||||
// The controller of the day picker. There are cases where the selected value
|
||||
// of the picker is invalid (e.g. February 30th 2018), and this dayController
|
||||
// is responsible for jumping to a valid value.
|
||||
FixedExtentScrollController dayController;
|
||||
|
||||
// Estimated width of columns.
|
||||
Map<int, double> estimatedColumnWidths = <int, double>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedDay = widget.initialDateTime.day;
|
||||
selectedMonth = widget.initialDateTime.month;
|
||||
selectedYear = widget.initialDateTime.year;
|
||||
|
||||
dayController = FixedExtentScrollController(initialItem: selectedDay - 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
|
||||
localizations = CupertinoLocalizations.of(context) ?? const DefaultCupertinoLocalizations();
|
||||
|
||||
alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
|
||||
alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
|
||||
|
||||
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
|
||||
estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
|
||||
estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
|
||||
}
|
||||
|
||||
Widget _buildDayPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
final int daysInCurrentMonth = DateTime(selectedYear, (selectedMonth + 1) % 12, 0).day;
|
||||
return CupertinoPicker(
|
||||
scrollController: dayController,
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedDay = index + 1;
|
||||
if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
|
||||
widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
|
||||
},
|
||||
children: List<Widget>.generate(31, (int index) {
|
||||
TextStyle disableTextStyle; // Null if not out of range.
|
||||
if (index >= daysInCurrentMonth) {
|
||||
disableTextStyle = const TextStyle(color: CupertinoColors.inactiveGray);
|
||||
}
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(
|
||||
localizations.datePickerDayOfMonth(index + 1),
|
||||
style: disableTextStyle,
|
||||
),
|
||||
);
|
||||
}),
|
||||
looping: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMonthPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker(
|
||||
scrollController: FixedExtentScrollController(initialItem: selectedMonth - 1),
|
||||
offAxisFraction: offAxisFraction,
|
||||
itemExtent: _kItemExtent,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedMonth = index + 1;
|
||||
if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
|
||||
widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
|
||||
},
|
||||
children: List<Widget>.generate(12, (int index) {
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(localizations.datePickerMonth(index + 1)),
|
||||
);
|
||||
}),
|
||||
looping: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildYearPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
|
||||
return CupertinoPicker.builder(
|
||||
scrollController: FixedExtentScrollController(initialItem: selectedYear),
|
||||
itemExtent: _kItemExtent,
|
||||
offAxisFraction: offAxisFraction,
|
||||
useMagnifier: _kUseMagnifier,
|
||||
magnification: _kMagnification,
|
||||
backgroundColor: _kBackgroundColor,
|
||||
onSelectedItemChanged: (int index) {
|
||||
selectedYear = index;
|
||||
if (DateTime(selectedYear, selectedMonth, selectedDay).day == selectedDay)
|
||||
widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
|
||||
},
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index < widget.minimumYear)
|
||||
return null;
|
||||
|
||||
if (widget.maximumYear != null && index > widget.maximumYear)
|
||||
return null;
|
||||
|
||||
return itemPositioningBuilder(
|
||||
context,
|
||||
Text(localizations.datePickerYear(index)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _keepInValidRange(ScrollEndNotification notification) {
|
||||
// Whenever scrolling lands on an invalid entry, the picker
|
||||
// automatically scrolls to a valid one.
|
||||
final int desiredDay = DateTime(selectedYear, selectedMonth, selectedDay).day;
|
||||
if (desiredDay != selectedDay) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
|
||||
dayController.animateToItem(
|
||||
// The next valid date is also the amount of days overflown.
|
||||
dayController.selectedItem - desiredDay,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
// Rebuild because the number of valid days per month are different
|
||||
// depending on the month and year.
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[];
|
||||
List<double> columnWidths = <double>[];
|
||||
|
||||
switch (localizations.datePickerDateOrder) {
|
||||
case DatePickerDateOrder.mdy:
|
||||
pickerBuilders = <_ColumnBuilder>[_buildMonthPicker, _buildDayPicker, _buildYearPicker];
|
||||
columnWidths = <double>[
|
||||
estimatedColumnWidths[_PickerColumnType.month.index],
|
||||
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
|
||||
estimatedColumnWidths[_PickerColumnType.year.index]];
|
||||
break;
|
||||
case DatePickerDateOrder.dmy:
|
||||
pickerBuilders = <_ColumnBuilder>[_buildDayPicker, _buildMonthPicker, _buildYearPicker];
|
||||
columnWidths = <double>[
|
||||
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
|
||||
estimatedColumnWidths[_PickerColumnType.month.index],
|
||||
estimatedColumnWidths[_PickerColumnType.year.index]];
|
||||
break;
|
||||
case DatePickerDateOrder.ymd:
|
||||
pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildMonthPicker, _buildDayPicker];
|
||||
columnWidths = <double>[
|
||||
estimatedColumnWidths[_PickerColumnType.year.index],
|
||||
estimatedColumnWidths[_PickerColumnType.month.index],
|
||||
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]];
|
||||
break;
|
||||
case DatePickerDateOrder.ydm:
|
||||
pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildDayPicker, _buildMonthPicker];
|
||||
columnWidths = <double>[
|
||||
estimatedColumnWidths[_PickerColumnType.year.index],
|
||||
estimatedColumnWidths[_PickerColumnType.dayOfMonth.index],
|
||||
estimatedColumnWidths[_PickerColumnType.month.index]];
|
||||
break;
|
||||
default:
|
||||
assert(false, 'date order is not specified');
|
||||
}
|
||||
|
||||
final List<Widget> pickers = <Widget>[];
|
||||
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
final double offAxisFraction = (i - 1) * 0.3 * textDirectionFactor;
|
||||
|
||||
EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
|
||||
if (textDirectionFactor == -1)
|
||||
padding = const EdgeInsets.only(left: _kDatePickerPadSize);
|
||||
|
||||
pickers.add(LayoutId(
|
||||
id: i,
|
||||
child: pickerBuilders[i](
|
||||
offAxisFraction,
|
||||
(BuildContext context, Widget child) {
|
||||
return Container(
|
||||
alignment: i == columnWidths.length - 1
|
||||
? alignCenterLeft
|
||||
: alignCenterRight,
|
||||
padding: i == 0 ? null : padding,
|
||||
child: Container(
|
||||
alignment: i == 0 ? alignCenterLeft : alignCenterRight,
|
||||
width: columnWidths[i] + _kDatePickerPadSize,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return MediaQuery(
|
||||
data: const MediaQueryData(textScaleFactor: 1.0),
|
||||
child: NotificationListener<ScrollEndNotification>(
|
||||
onNotification: _keepInValidRange,
|
||||
child: CustomMultiChildLayout(
|
||||
delegate: _DatePickerLayoutDelegate(
|
||||
columnWidths: columnWidths,
|
||||
textDirectionFactor: textDirectionFactor,
|
||||
),
|
||||
children: pickers,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The iOS date picker and timer picker has their width fixed to 330.0 in all
|
||||
// modes.
|
||||
//
|
||||
// If the maximum width given to the picker is greater than 330.0, the leftmost
|
||||
// and rightmost column will be extended equally so that the widths match, and
|
||||
// the picker is in the center.
|
||||
//
|
||||
// If the maximum width given to the picker is smaller than 330.0, the picker is
|
||||
// placed in the center and both left side and right side are clipped.
|
||||
// If the maximum width given to the picker is smaller than 330.0, the picker's
|
||||
// layout will be broken.
|
||||
|
||||
|
||||
/// Different modes of [CupertinoTimerPicker].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
|
||||
/// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
|
||||
enum CupertinoTimerPickerMode {
|
||||
/// Mode that shows the timer duration in hour and minute.
|
||||
///
|
||||
@ -47,14 +923,20 @@ enum CupertinoTimerPickerMode {
|
||||
/// The duration is bound between 0 and 23 hours 59 minutes 59 seconds.
|
||||
///
|
||||
/// There are several modes of the timer picker listed in [CupertinoTimerPickerMode].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoDatePicker], the class that implements different display modes
|
||||
/// of the iOS-style date picker.
|
||||
/// * [CupertinoPicker], the class that implements a content agnostic spinner UI.
|
||||
class CupertinoTimerPicker extends StatefulWidget {
|
||||
/// Constructs an iOS style countdown timer picker.
|
||||
///
|
||||
/// [mode] is one of the modes listed in [CupertinoTimerPickerMode] and
|
||||
/// defaults to [CupertinoTimerPickerMode.hms].
|
||||
///
|
||||
/// [onTimerDurationChanged] is the callback when the selected duration changes
|
||||
/// and must not be null.
|
||||
/// [onTimerDurationChanged] is the callback called when the selected duration
|
||||
/// changes and must not be null.
|
||||
///
|
||||
/// [initialTimerDuration] defaults to 0 second and is limited from 0 second
|
||||
/// to 23 hours 59 minutes 59 seconds.
|
||||
@ -93,7 +975,7 @@ class CupertinoTimerPicker extends StatefulWidget {
|
||||
/// of 60.
|
||||
final int secondInterval;
|
||||
|
||||
/// Callback when the timer duration changes.
|
||||
/// Callback called when the timer duration changes.
|
||||
final ValueChanged<Duration> onTimerDurationChanged;
|
||||
|
||||
@override
|
||||
@ -304,8 +1186,7 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
minuteLabel = IgnorePointer(
|
||||
child: Container(
|
||||
alignment: alignCenterRight,
|
||||
@ -420,16 +1301,14 @@ class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
|
||||
Expanded(child: _buildMinuteColumn()),
|
||||
],
|
||||
);
|
||||
}
|
||||
else if (widget.mode == CupertinoTimerPickerMode.ms) {
|
||||
} else if (widget.mode == CupertinoTimerPickerMode.ms) {
|
||||
picker = Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: _buildMinuteColumn()),
|
||||
Expanded(child: _buildSecondColumn()),
|
||||
],
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
picker = Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: _buildHourColumn()),
|
||||
|
||||
@ -7,6 +7,46 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Determines the order of the columns inside [CupertinoDatePicker] in
|
||||
/// time and date time mode.
|
||||
enum DatePickerDateTimeOrder {
|
||||
/// Order of the columns, from left to right: date, hour, minute, am/pm.
|
||||
///
|
||||
/// Example: [Fri Aug 31 | 02 | 08 | PM].
|
||||
date_time_dayPeriod,
|
||||
/// Order of the columns, from left to right: date, am/pm, hour, minute.
|
||||
///
|
||||
/// Example: [Fri Aug 31 | PM | 02 | 08].
|
||||
date_dayPeriod_time,
|
||||
/// Order of the columns, from left to right: hour, minute, am/pm, date.
|
||||
///
|
||||
/// Example: [02 | 08 | PM | Fri Aug 31].
|
||||
time_dayPeriod_date,
|
||||
/// Order of the columns, from left to right: am/pm, hour, minute, date.
|
||||
///
|
||||
/// Example: [PM | 02 | 08 | Fri Aug 31].
|
||||
dayPeriod_time_date,
|
||||
}
|
||||
|
||||
/// Determines the order of the columns inside [CupertinoDatePicker] in date mode.
|
||||
enum DatePickerDateOrder {
|
||||
/// Order of the columns, from left to right: day, month, year.
|
||||
///
|
||||
/// Example: [12 | March | 1996]
|
||||
dmy,
|
||||
/// Order of the columns, from left to right: month, day, year.
|
||||
///
|
||||
/// Example: [March | 12 | 1996]
|
||||
mdy,
|
||||
/// Order of the columns, from left to right: year, month, day.
|
||||
///
|
||||
/// Example: [1996 | March | 12]
|
||||
ymd,
|
||||
/// Order of the columns, from left to right: year, day, month.
|
||||
///
|
||||
/// Example: [1996 | 12 | March]
|
||||
ydm,
|
||||
}
|
||||
|
||||
/// Defines the localized resource values used by the Cupertino widgets.
|
||||
///
|
||||
@ -61,6 +101,9 @@ abstract class CupertinoLocalizations {
|
||||
/// - Arabic: ٠١
|
||||
String datePickerHour(int hour);
|
||||
|
||||
/// Semantics label for the given hour value in [CupertinoDatePicker].
|
||||
String datePickerHourSemanticsLabel(int hour);
|
||||
|
||||
/// Minute that is shown in [CupertinoDatePicker] spinner corresponding
|
||||
/// to the given minute value.
|
||||
///
|
||||
@ -70,9 +113,14 @@ abstract class CupertinoLocalizations {
|
||||
/// - Arabic: ٠١
|
||||
String datePickerMinute(int minute);
|
||||
|
||||
/// Semantics label for the given minute value in [CupertinoDatePicker].
|
||||
String datePickerMinuteSemanticsLabel(int minute);
|
||||
|
||||
/// The order of the date elements that will be shown in [CupertinoDatePicker].
|
||||
/// Can be any permutation of 'DMY' ('D': day, 'M': month, 'Y': year).
|
||||
String get datePickerDateOrder;
|
||||
DatePickerDateOrder get datePickerDateOrder;
|
||||
|
||||
/// The order of the time elements that will be shown in [CupertinoDatePicker].
|
||||
DatePickerDateTimeOrder get datePickerDateTimeOrder;
|
||||
|
||||
/// The abbreviation for ante meridiem (before noon) shown in the time picker.
|
||||
String get anteMeridiemAbbreviation;
|
||||
@ -216,9 +264,19 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
|
||||
@override
|
||||
String datePickerHour(int hour) => hour.toString().padLeft(2, '0');
|
||||
|
||||
@override
|
||||
String datePickerHourSemanticsLabel(int hour) => hour.toString() + " o'clock";
|
||||
|
||||
@override
|
||||
String datePickerMinute(int minute) => minute.toString().padLeft(2, '0');
|
||||
|
||||
@override
|
||||
String datePickerMinuteSemanticsLabel(int minute) {
|
||||
if (minute == 1)
|
||||
return '1 minute';
|
||||
return minute.toString() + ' minutes';
|
||||
}
|
||||
|
||||
@override
|
||||
String datePickerMediumDate(DateTime date) {
|
||||
return '${_shortWeekdays[date.weekday - DateTime.monday]} '
|
||||
@ -227,7 +285,10 @@ class DefaultCupertinoLocalizations implements CupertinoLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get datePickerDateOrder => 'MDY';
|
||||
DatePickerDateOrder get datePickerDateOrder => DatePickerDateOrder.mdy;
|
||||
|
||||
@override
|
||||
DatePickerDateTimeOrder get datePickerDateTimeOrder => DatePickerDateTimeOrder.date_time_dayPeriod;
|
||||
|
||||
@override
|
||||
String get anteMeridiemAbbreviation => 'AM';
|
||||
|
||||
@ -97,36 +97,6 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('secondInterval is positive and is a factor of 60', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
CupertinoTimerPicker(
|
||||
onTimerDurationChanged: (_) {},
|
||||
secondInterval: 0,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
expect(
|
||||
() {
|
||||
CupertinoTimerPicker(
|
||||
onTimerDurationChanged: (_) {},
|
||||
secondInterval: -1,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
expect(
|
||||
() {
|
||||
CupertinoTimerPicker(
|
||||
onTimerDurationChanged: (_) {},
|
||||
secondInterval: 7,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('columns are ordered correctly when text direction is ltr', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
@ -223,4 +193,364 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
group('Date picker', () {
|
||||
testWidgets('mode is not null', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
CupertinoDatePicker(
|
||||
mode: null,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime.now(),
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('onDateTimeChanged is not null', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
CupertinoDatePicker(
|
||||
onDateTimeChanged: null,
|
||||
initialDateTime: DateTime.now(),
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('initial date time is not null', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
CupertinoDatePicker(
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: null,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('initial date time is not null', (WidgetTester tester) async {
|
||||
expect(
|
||||
() {
|
||||
CupertinoDatePicker(
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: null,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('changing initialDateTime after first build does not do anything', (WidgetTester tester) async {
|
||||
DateTime selectedDateTime;
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime,
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.text('10'), const Offset(0.0, 32.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(selectedDateTime, DateTime(2018, 1, 1, 9, 30));
|
||||
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime,
|
||||
// Change the initial date, but it shouldn't affect the present state.
|
||||
initialDateTime: DateTime(2016, 4, 5, 15, 00),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.text('09'), const Offset(0.0, 32.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
// Moving up an hour is still based on the original initial date time.
|
||||
expect(selectedDateTime, DateTime(2018, 1, 1, 8, 30));
|
||||
});
|
||||
|
||||
testWidgets('width of picker in date and time mode is consistent', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first column and the last column.
|
||||
final double distance =
|
||||
tester.getCenter(find.text('Mon Jan 1')).dx - tester.getCenter(find.text('AM')).dx;
|
||||
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 800.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.dateAndTime,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first and the last column should be the same.
|
||||
expect(
|
||||
tester.getCenter(find.text('Mon Jan 1')).dx - tester.getCenter(find.text('AM')).dx,
|
||||
distance,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('width of picker in date mode is consistent', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first column and the last column.
|
||||
final double distance =
|
||||
tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx;
|
||||
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 800.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first and the last column should be the same.
|
||||
expect(
|
||||
tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx,
|
||||
distance,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('width of picker in time mode is consistent', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first column and the last column.
|
||||
final double distance =
|
||||
tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx;
|
||||
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 800.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
onDateTimeChanged: (_) {},
|
||||
initialDateTime: DateTime(2018, 1, 1, 10, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Distance between the first and the last column should be the same.
|
||||
expect(
|
||||
tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx,
|
||||
distance,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('picker automatically scrolls away from invalid date on month change', (WidgetTester tester) async {
|
||||
DateTime date;
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
date = newDate;
|
||||
},
|
||||
initialDateTime: DateTime(2018, 3, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.text('March'), const Offset(0.0, 32.0));
|
||||
// Momentarily, the 2018 and the incorrect 30 of February is aligned.
|
||||
expect(
|
||||
tester.getTopLeft(find.text('2018')).dy,
|
||||
tester.getTopLeft(find.text('30')).dy,
|
||||
);
|
||||
await tester.pump(); // Once to trigger the post frame animate call.
|
||||
await tester.pump(); // Once to start the DrivenScrollActivity.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
date,
|
||||
DateTime(2018, 2, 28),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(find.text('2018')).dy,
|
||||
tester.getTopLeft(find.text('28')).dy,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('picker automatically scrolls away from invalid date on day change', (WidgetTester tester) async {
|
||||
DateTime date;
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
date = newDate;
|
||||
},
|
||||
initialDateTime: DateTime(2018, 2, 27), // 2018 has 28 days in Feb.
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.text('27'), const Offset(0.0, -32.0));
|
||||
await tester.pump();
|
||||
expect(
|
||||
date,
|
||||
DateTime(2018, 2, 28),
|
||||
);
|
||||
|
||||
|
||||
await tester.drag(find.text('28'), const Offset(0.0, -32.0));
|
||||
await tester.pump(); // Once to trigger the post frame animate call.
|
||||
|
||||
// Callback doesn't transiently go into invalid dates.
|
||||
expect(
|
||||
date,
|
||||
DateTime(2018, 2, 28),
|
||||
);
|
||||
// Momentarily, the invalid 29th of Feb is dragged into the middle.
|
||||
expect(
|
||||
tester.getTopLeft(find.text('2018')).dy,
|
||||
tester.getTopLeft(find.text('29')).dy,
|
||||
);
|
||||
|
||||
await tester.pump(); // Once to start the DrivenScrollActivity.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
date,
|
||||
DateTime(2018, 2, 28),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(find.text('2018')).dy,
|
||||
tester.getTopLeft(find.text('28')).dy,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('picker automatically scrolls the am/pm column when the hour column changes enough', (WidgetTester tester) async {
|
||||
DateTime date;
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
height: 400.0,
|
||||
width: 400.0,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
date = newDate;
|
||||
},
|
||||
initialDateTime: DateTime(2018, 1, 1, 11, 59),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.text('11'), const Offset(0.0, -32.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(date, DateTime(2018, 1, 1, 12, 59));
|
||||
|
||||
await tester.drag(find.text('12'), const Offset(0.0, 32.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(date, DateTime(2018, 1, 1, 11, 59));
|
||||
|
||||
await tester.drag(find.text('11'), const Offset(0.0, 64.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(date, DateTime(2018, 1, 1, 9, 59));
|
||||
|
||||
await tester.drag(find.text('09'), const Offset(0.0, -192.0));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(date, DateTime(2018, 1, 1, 15, 59));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -13,9 +13,12 @@ void main() {
|
||||
expect(localizations.datePickerMonth(1), isNotNull);
|
||||
expect(localizations.datePickerDayOfMonth(1), isNotNull);
|
||||
expect(localizations.datePickerHour(0), isNotNull);
|
||||
expect(localizations.datePickerHourSemanticsLabel(0), isNotNull);
|
||||
expect(localizations.datePickerMinute(0), isNotNull);
|
||||
expect(localizations.datePickerMinuteSemanticsLabel(0), isNotNull);
|
||||
expect(localizations.datePickerMediumDate(DateTime.now()), isNotNull);
|
||||
expect(localizations.datePickerDateOrder, isNotNull);
|
||||
expect(localizations.datePickerDateTimeOrder, isNotNull);
|
||||
|
||||
expect(localizations.anteMeridiemAbbreviation, isNotNull);
|
||||
expect(localizations.postMeridiemAbbreviation, isNotNull);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user