Commit 2a8e35cc authored by Viet Do's avatar Viet Do Committed by xster

Cupertino Date Picker (#21251)

parent efc5123d
......@@ -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);
},
),
);
......@@ -123,11 +152,101 @@ class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
'${(timer.inSeconds % 60).toString().padLeft(2,'0')}',
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),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
......@@ -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 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,
);
}
// The iOS timer picker has its width fixed to 330.0 in all modes.
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));
});
});
}
\ No newline at end of file
......@@ -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);
......
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