Unverified Commit adc64827 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Implement CupertinoDatepicker time/dateTime constraints (#44628)

parent a3bbdfb2
...@@ -48,6 +48,14 @@ TextStyle _themeTextStyle(BuildContext context, { bool isValid = true }) { ...@@ -48,6 +48,14 @@ TextStyle _themeTextStyle(BuildContext context, { bool isValid = true }) {
return isValid ? style : style.copyWith(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context)); return isValid ? style : style.copyWith(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context));
} }
void _animateColumnControllerToItem(FixedExtentScrollController controller, int targetItem) {
controller.animateToItem(
targetItem,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200),
);
}
// Lays out the date picker based on how much space each single column needs. // 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 is a child of this delegate, indexed from 0 to number of columns - 1.
...@@ -193,20 +201,23 @@ class CupertinoDatePicker extends StatefulWidget { ...@@ -193,20 +201,23 @@ class CupertinoDatePicker extends StatefulWidget {
/// to [CupertinoDatePickerMode.dateAndTime]. /// to [CupertinoDatePickerMode.dateAndTime].
/// ///
/// [onDateTimeChanged] is the callback called when the selected date or time /// [onDateTimeChanged] is the callback called when the selected date or time
/// changes and must not be null. /// changes and must not be null. When in [CupertinoDatePickerMode.time] mode,
/// the year, month and day will be the same as [initialDateTime].
/// ///
/// [initialDateTime] is the initial date time of the picker. Defaults to the /// [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 /// present date and time and must not be null. The present must conform to
/// the intervals set in [minimumDate], [maximumDate], [minimumYear], and /// the intervals set in [minimumDate], [maximumDate], [minimumYear], and
/// [maximumYear]. /// [maximumYear].
/// ///
/// [minimumDate] is the minimum date that the picker can be scrolled to in /// [minimumDate] is the minimum [DateTime] that the picker can be scrolled to.
/// [CupertinoDatePickerMode.date] and [CupertinoDatePickerMode.dateAndTime] /// Null if there's no limit. In [CupertinoDatePickerMode.time] mode, if the
/// mode. Null if there's no limit. /// date part of [initialDateTime] is after that of the [minimumDate], [minimumDate]
/// has no effect.
/// ///
/// [maximumDate] is the maximum date that the picker can be scrolled to in /// [maximumDate] is the maximum [DateTime] that the picker can be scrolled to.
/// [CupertinoDatePickerMode.date] and [CupertinoDatePickerMode.dateAndTime] /// Null if there's no limit. In [CupertinoDatePickerMode.time] mode, if the
/// mode. Null if there's no limit. /// date part of [initialDateTime] is before that of the [maximumDate], [maximumDate]
/// has no effect.
/// ///
/// [minimumYear] is the minimum year that the picker can be scrolled to in /// [minimumYear] is the minimum year that the picker can be scrolled to in
/// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null. /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
...@@ -429,32 +440,82 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -429,32 +440,82 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
DateTime initialDateTime; DateTime initialDateTime;
// The difference in days between the initial date and the currently selected date. // The difference in days between the initial date and the currently selected date.
int selectedDayFromInitial; // 0 if the current mode does not involve a date.
int get selectedDayFromInitial {
// The current selection of the hour picker. switch (widget.mode) {
// case CupertinoDatePickerMode.dateAndTime:
// If [widget.use24hFormat] is true, values range from 1-24. Otherwise values return dateController.hasClients ? dateController.selectedItem : 0;
// range from 1-12. case CupertinoDatePickerMode.time:
int selectedHour; return 0;
case CupertinoDatePickerMode.date:
break;
}
assert(
false,
'$runtimeType is only meant for dateAndTime mode or time mode',
);
return 0;
}
// The controller of the date column.
FixedExtentScrollController dateController;
// The previous selection index of the hour column. // The current selection of the hour picker. Values range from 0 to 23.
// int get selectedHour => _selectedHour(selectedAmPm, _selectedHourIndex);
// This ranges from 0-23 even if [widget.use24hFormat] is false. As a result, int get _selectedHourIndex => hourController.hasClients ? hourController.selectedItem % 24 : initialDateTime.hour;
// it can be used for determining if we just changed from AM -> PM or vice // Calculates the selected hour given the selected indices of the hour picker
// versa. // and the meridiem picker.
int previousHourIndex; int _selectedHour(int selectedAmPm, int selectedHour) {
return _isHourRegionFlipped(selectedAmPm) ? (selectedHour + 12) % 24 : selectedHour;
}
// The controller of the hour column.
FixedExtentScrollController hourController;
// The current selection of the minute picker. Values range from 0 to 59. // The current selection of the minute picker. Values range from 0 to 59.
int selectedMinute; int get selectedMinute {
return minuteController.hasClients
? minuteController.selectedItem * widget.minuteInterval % 60
: initialDateTime.minute;
}
// The controller of the minute column.
FixedExtentScrollController minuteController;
// Whether the current meridiem selection is AM or PM.
//
// We can't use the selectedItem of meridiemController as the source of truth
// because the meridiem picker can be scrolled **animatedly** by the hour picker
// (e.g. if you scroll from 12 to 1 in 12h format), but the meridiem change
// should take effect immediately, **before** the animation finishes.
int selectedAmPm;
// Whether the physical-region-to-meridiem mapping is flipped.
bool get isHourRegionFlipped => _isHourRegionFlipped(selectedAmPm);
bool _isHourRegionFlipped(int selectedAmPm) => selectedAmPm != meridiemRegion;
// The index of the 12-hour region the hour picker is currently in.
//
// Used to determine whether the meridiemController should start animating.
// Valid values are 0 and 1.
//
// The AM/PM correspondence of the two regions flips when the meridiem picker
// scrolls. This variable is to keep track of the selected "physical"
// (meridiem picker invariant) region of the hour picker. The "physical" region
// of an item of index `i` is `i ~/ 12`.
int meridiemRegion;
// The current selection of the AM/PM picker. // The current selection of the AM/PM picker.
// //
// - 0 means AM // - 0 means AM
// - 1 means PM // - 1 means PM
int selectedAmPm; FixedExtentScrollController meridiemController;
bool isDatePickerScrolling = false;
bool isHourPickerScrolling = false;
bool isMinutePickerScrolling = false;
bool isMeridiemPickerScrolling = false;
// The controller of the AM/PM column. bool get isScrolling {
FixedExtentScrollController amPmController; return isDatePickerScrolling
|| isHourPickerScrolling
|| isMinutePickerScrolling
|| isMeridiemPickerScrolling;
}
// The estimated width of columns. // The estimated width of columns.
final Map<int, double> estimatedColumnWidths = <int, double>{}; final Map<int, double> estimatedColumnWidths = <int, double>{};
...@@ -463,21 +524,18 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -463,21 +524,18 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
void initState() { void initState() {
super.initState(); super.initState();
initialDateTime = widget.initialDateTime; initialDateTime = widget.initialDateTime;
selectedDayFromInitial = 0;
selectedHour = widget.initialDateTime.hour;
selectedMinute = widget.initialDateTime.minute;
selectedAmPm = 0;
if (!widget.use24hFormat) { // Initially each of the "physical" regions is mapped to the meridiem region
selectedAmPm = selectedHour ~/ 12; // with the same number, e.g., the first 12 items are mapped to the first 12
selectedHour = selectedHour % 12; // hours of a day. Such mapping is flipped when the meridiem picker is scrolled
if (selectedHour == 0) // by the user, the first 12 items are mapped to the last 12 hours of a day.
selectedHour = 12; selectedAmPm = initialDateTime.hour ~/ 12;
meridiemRegion = selectedAmPm;
amPmController = FixedExtentScrollController(initialItem: selectedAmPm);
}
previousHourIndex = selectedHour; meridiemController = FixedExtentScrollController(initialItem: selectedAmPm);
hourController = FixedExtentScrollController(initialItem: initialDateTime.hour);
minuteController = FixedExtentScrollController(initialItem: initialDateTime.minute ~/ widget.minuteInterval);
dateController = FixedExtentScrollController(initialItem: 0);
PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange); PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange);
} }
...@@ -493,6 +551,11 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -493,6 +551,11 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
@override @override
void dispose() { void dispose() {
dateController.dispose();
hourController.dispose();
minuteController.dispose();
meridiemController.dispose();
PaintingBinding.instance.systemFonts.removeListener(_handleSystemFontsChange); PaintingBinding.instance.systemFonts.removeListener(_handleSystemFontsChange);
super.dispose(); super.dispose();
} }
...@@ -503,8 +566,15 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -503,8 +566,15 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
assert( assert(
oldWidget.mode == widget.mode, oldWidget.mode == widget.mode,
"The CupertinoDatePicker's mode cannot change once it's built", "The $runtimeType's mode cannot change once it's built.",
); );
if (!widget.use24hFormat && oldWidget.use24hFormat) {
// Thanks to the physical and meridiem region mapping, the only thing we
// need to update is the meridiem controller, if it's not previously attached.
meridiemController.dispose();
meridiemController = FixedExtentScrollController(initialItem: selectedAmPm);
}
} }
@override @override
...@@ -531,26 +601,44 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -531,26 +601,44 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
} }
// Gets the current date time of the picker. // Gets the current date time of the picker.
DateTime _getDateTime() { DateTime get selectedDateTime {
final DateTime date = DateTime( return DateTime(
initialDateTime.year, initialDateTime.year,
initialDateTime.month, initialDateTime.month,
initialDateTime.day, initialDateTime.day + selectedDayFromInitial,
).add(Duration(days: selectedDayFromInitial)); selectedHour,
return DateTime(
date.year,
date.month,
date.day,
widget.use24hFormat ? selectedHour : selectedHour % 12 + selectedAmPm * 12,
selectedMinute, selectedMinute,
); );
} }
// Only reports datetime change when the date time is valid.
void _onSelectedItemChange(int index) {
final DateTime selected = selectedDateTime;
final bool isDateInvalid = widget.minimumDate?.isAfter(selected) == true
|| widget.maximumDate?.isBefore(selected) == true;
if (isDateInvalid)
return;
widget.onDateTimeChanged(selected);
}
// Builds the date column. The date is displayed in medium date format (e.g. Fri Aug 31). // Builds the date column. The date is displayed in medium date format (e.g. Fri Aug 31).
Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) { Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
return CupertinoPicker.builder( return NotificationListener<ScrollNotification>(
scrollController: FixedExtentScrollController(initialItem: selectedDayFromInitial), onNotification: (ScrollNotification notification) {
if (notification is ScrollStartNotification) {
isDatePickerScrolling = true;
} else if (notification is ScrollEndNotification) {
isDatePickerScrolling = false;
_pickerDidStopScrolling();
}
return false;
},
child: CupertinoPicker.builder(
scrollController: dateController,
offAxisFraction: offAxisFraction, offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
useMagnifier: _kUseMagnifier, useMagnifier: _kUseMagnifier,
...@@ -558,44 +646,75 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -558,44 +646,75 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
squeeze: _kSqueeze, squeeze: _kSqueeze,
onSelectedItemChanged: (int index) { onSelectedItemChanged: (int index) {
selectedDayFromInitial = index; _onSelectedItemChange(index);
widget.onDateTimeChanged(_getDateTime());
}, },
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final DateTime dateTime = DateTime( final DateTime rangeStart = DateTime(
initialDateTime.year, initialDateTime.year,
initialDateTime.month, initialDateTime.month,
initialDateTime.day, initialDateTime.day + index,
).add(Duration(days: index)); );
if (widget.minimumDate != null && dateTime.isBefore(widget.minimumDate)) // Exclusive.
return null; final DateTime rangeEnd = DateTime(
if (widget.maximumDate != null && dateTime.isAfter(widget.maximumDate)) initialDateTime.year,
return null; initialDateTime.month,
initialDateTime.day + index + 1,
);
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
String dateText;
if (dateTime == DateTime(now.year, now.month, now.day)) { if (widget.minimumDate?.isAfter(rangeEnd) == true)
dateText = localizations.todayLabel; return null;
} else { if (widget.maximumDate?.isAfter(rangeStart) == false)
dateText = localizations.datePickerMediumDate(dateTime); return null;
}
final String dateText = rangeStart == DateTime(now.year, now.month, now.day)
? localizations.todayLabel
: localizations.datePickerMediumDate(rangeStart);
return itemPositioningBuilder( return itemPositioningBuilder(
context, context,
Text( Text(dateText, style: _themeTextStyle(context)),
dateText,
style: _themeTextStyle(context),
),
); );
}, },
),
); );
} }
// With the meridem picker set to `meridiemIndex`, and the hour picker set to
// `hourIndex`, is it possible to change the value of the minute picker, so
// that the resulting date stays in the valid range.
bool _isValidHour(int meridiemIndex, int hourIndex) {
final DateTime rangeStart = DateTime(
initialDateTime.year,
initialDateTime.month,
initialDateTime.day + selectedDayFromInitial,
_selectedHour(meridiemIndex, hourIndex),
0,
);
// The end value of the range is exclusive, i.e. [rangeStart, rangeEnd).
final DateTime rangeEnd = rangeStart.add(const Duration(hours: 1));
return (widget.minimumDate?.isBefore(rangeEnd) ?? true)
&& !(widget.maximumDate?.isBefore(rangeStart) ?? false);
}
Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) { Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
return CupertinoPicker( return NotificationListener<ScrollNotification>(
scrollController: FixedExtentScrollController(initialItem: selectedHour), onNotification: (ScrollNotification notification) {
if (notification is ScrollStartNotification) {
isHourPickerScrolling = true;
} else if (notification is ScrollEndNotification) {
isHourPickerScrolling = false;
_pickerDidStopScrolling();
}
return false;
},
child: CupertinoPicker(
scrollController: hourController,
offAxisFraction: offAxisFraction, offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
useMagnifier: _kUseMagnifier, useMagnifier: _kUseMagnifier,
...@@ -603,81 +722,112 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -603,81 +722,112 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
squeeze: _kSqueeze, squeeze: _kSqueeze,
onSelectedItemChanged: (int index) { onSelectedItemChanged: (int index) {
if (widget.use24hFormat) { final bool regionChanged = meridiemRegion != index ~/ 12;
selectedHour = index; final bool debugIsFlipped = isHourRegionFlipped;
widget.onDateTimeChanged(_getDateTime());
} else {
selectedHour = index % 12;
// Automatically scrolls the am/pm column when the hour column value
// goes far enough.
final bool wasAm = previousHourIndex >=0 && previousHourIndex <= 11; if (regionChanged) {
final bool isAm = index >= 0 && index <= 11; meridiemRegion = index ~/ 12;
selectedAmPm = 1 - selectedAmPm;
}
if (wasAm != isAm) { if (!widget.use24hFormat && regionChanged) {
// Scroll the meridiem column to adjust AM/PM.
//
// _onSelectedItemChanged will be called when the animation finishes.
//
// Animation values obtained by comparing with iOS version. // Animation values obtained by comparing with iOS version.
amPmController.animateToItem( meridiemController.animateToItem(
1 - amPmController.selectedItem, selectedAmPm,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} else { } else {
widget.onDateTimeChanged(_getDateTime()); _onSelectedItemChange(index);
}
} }
previousHourIndex = index; assert(debugIsFlipped == isHourRegionFlipped);
}, },
children: List<Widget>.generate(24, (int index) { children: List<Widget>.generate(24, (int index) {
int hour = index; final int hour = isHourRegionFlipped ? (index + 12) % 24 : index;
if (!widget.use24hFormat) final int displayHour = widget.use24hFormat ? hour : (hour + 11) % 12 + 1;
hour = hour % 12 == 0 ? 12 : hour % 12;
return itemPositioningBuilder( return itemPositioningBuilder(
context, context,
Text( Text(
localizations.datePickerHour(hour), localizations.datePickerHour(displayHour),
semanticsLabel: localizations.datePickerHourSemanticsLabel(hour), semanticsLabel: localizations.datePickerHourSemanticsLabel(displayHour),
style: _themeTextStyle(context), style: _themeTextStyle(context, isValid: _isValidHour(selectedAmPm, index)),
), ),
); );
}), }),
looping: true, looping: true,
)
); );
} }
Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) { Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
return CupertinoPicker( return NotificationListener<ScrollNotification>(
scrollController: FixedExtentScrollController(initialItem: selectedMinute ~/ widget.minuteInterval), onNotification: (ScrollNotification notification) {
if (notification is ScrollStartNotification) {
isMinutePickerScrolling = true;
} else if (notification is ScrollEndNotification) {
isMinutePickerScrolling = false;
_pickerDidStopScrolling();
}
return false;
},
child: CupertinoPicker(
scrollController: minuteController,
offAxisFraction: offAxisFraction, offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
useMagnifier: _kUseMagnifier, useMagnifier: _kUseMagnifier,
magnification: _kMagnification, magnification: _kMagnification,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
squeeze: _kSqueeze, squeeze: _kSqueeze,
onSelectedItemChanged: (int index) { onSelectedItemChanged: _onSelectedItemChange,
selectedMinute = index * widget.minuteInterval;
widget.onDateTimeChanged(_getDateTime());
},
children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) { children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) {
final int minute = index * widget.minuteInterval; final int minute = index * widget.minuteInterval;
final DateTime date = DateTime(
initialDateTime.year,
initialDateTime.month,
initialDateTime.day + selectedDayFromInitial,
selectedHour,
minute,
);
final bool isInvalidMinute = (widget.minimumDate?.isAfter(date) ?? false)
|| (widget.maximumDate?.isBefore(date) ?? false);
return itemPositioningBuilder( return itemPositioningBuilder(
context, context,
Text( Text(
localizations.datePickerMinute(minute), localizations.datePickerMinute(minute),
semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute), semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute),
style: _themeTextStyle(context), style: _themeTextStyle(context, isValid: !isInvalidMinute),
), ),
); );
}), }),
looping: true, looping: true,
),
); );
} }
Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) { Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder) {
return CupertinoPicker( return NotificationListener<ScrollNotification>(
scrollController: amPmController, onNotification: (ScrollNotification notification) {
if (notification is ScrollStartNotification) {
isMeridiemPickerScrolling = true;
} else if (notification is ScrollEndNotification) {
isMeridiemPickerScrolling = false;
_pickerDidStopScrolling();
}
return false;
},
child: CupertinoPicker(
scrollController: meridiemController,
offAxisFraction: offAxisFraction, offAxisFraction: offAxisFraction,
itemExtent: _kItemExtent, itemExtent: _kItemExtent,
useMagnifier: _kUseMagnifier, useMagnifier: _kUseMagnifier,
...@@ -686,7 +836,8 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -686,7 +836,8 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
squeeze: _kSqueeze, squeeze: _kSqueeze,
onSelectedItemChanged: (int index) { onSelectedItemChanged: (int index) {
selectedAmPm = index; selectedAmPm = index;
widget.onDateTimeChanged(_getDateTime()); assert(selectedAmPm == 0 || selectedAmPm == 1);
_onSelectedItemChange(index);
}, },
children: List<Widget>.generate(2, (int index) { children: List<Widget>.generate(2, (int index) {
return itemPositioningBuilder( return itemPositioningBuilder(
...@@ -695,12 +846,67 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> { ...@@ -695,12 +846,67 @@ class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
index == 0 index == 0
? localizations.anteMeridiemAbbreviation ? localizations.anteMeridiemAbbreviation
: localizations.postMeridiemAbbreviation, : localizations.postMeridiemAbbreviation,
style: _themeTextStyle(context), style: _themeTextStyle(context, isValid: _isValidHour(index, _selectedHourIndex)),
), ),
); );
}), }),
),
);
}
// One or more pickers have just stopped scrolling.
void _pickerDidStopScrolling() {
// Call setState to update the greyed out date/hour/minute/meridiem.
setState(() { });
if (isScrolling)
return;
// Whenever scrolling lands on an invalid entry, the picker
// automatically scrolls to a valid one.
final DateTime selectedDate = selectedDateTime;
final bool minCheck = widget.minimumDate?.isAfter(selectedDate) ?? false;
final bool maxCheck = widget.maximumDate?.isBefore(selectedDate) ?? false;
if (minCheck || maxCheck) {
// We have minCheck === !maxCheck.
final DateTime targetDate = minCheck ? widget.minimumDate : widget.maximumDate;
_scrollToDate(targetDate, selectedDate);
}
}
void _scrollToDate(DateTime newDate, DateTime fromDate) {
assert(newDate != null);
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
if (fromDate.year != newDate.year || fromDate.month != newDate.month || fromDate.day != newDate.day) {
_animateColumnControllerToItem(dateController, selectedDayFromInitial);
}
if (fromDate.hour != newDate.hour) {
final bool needsMeridiemChange = !widget.use24hFormat
&& fromDate.hour ~/ 12 != newDate.hour ~/ 12;
// In AM/PM mode, the pickers should not scroll all the way to the other hour region.
if (needsMeridiemChange) {
_animateColumnControllerToItem(meridiemController, 1 - meridiemController.selectedItem);
// Keep the target item index in the current 12-h region.
final int newItem = (hourController.selectedItem ~/ 12) * 12
+ (hourController.selectedItem + newDate.hour - fromDate.hour) % 12;
_animateColumnControllerToItem(hourController, newItem);
} else {
_animateColumnControllerToItem(
hourController,
hourController.selectedItem + newDate.hour - fromDate.hour,
); );
} }
}
if (fromDate.minute != newDate.minute) {
_animateColumnControllerToItem(minuteController, newDate.minute);
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -944,8 +1150,8 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> { ...@@ -944,8 +1150,8 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
}, },
children: List<Widget>.generate(12, (int index) { children: List<Widget>.generate(12, (int index) {
final int month = index + 1; final int month = index + 1;
final bool isInvalidMonth = (widget?.minimumDate?.year == selectedYear && widget.minimumDate.month > month) final bool isInvalidMonth = (widget.minimumDate?.year == selectedYear && widget.minimumDate.month > month)
|| (widget?.maximumDate?.year == selectedYear && widget.maximumDate.month < month); || (widget.maximumDate?.year == selectedYear && widget.maximumDate.month < month);
return itemPositioningBuilder( return itemPositioningBuilder(
context, context,
...@@ -991,8 +1197,8 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> { ...@@ -991,8 +1197,8 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
if (widget.maximumYear != null && year > widget.maximumYear) if (widget.maximumYear != null && year > widget.maximumYear)
return null; return null;
final bool isValidYear = (widget?.minimumDate == null || widget.minimumDate.year <= year) final bool isValidYear = (widget.minimumDate == null || widget.minimumDate.year <= year)
&& (widget?.maximumDate == null || widget.maximumDate.year >= year); && (widget.maximumDate == null || widget.maximumDate.year >= year);
return itemPositioningBuilder( return itemPositioningBuilder(
context, context,
...@@ -1051,27 +1257,15 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> { ...@@ -1051,27 +1257,15 @@ class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
assert(newDate != null); assert(newDate != null);
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
if (selectedYear != newDate.year) { if (selectedYear != newDate.year) {
yearController.animateToItem( _animateColumnControllerToItem(yearController, newDate.year);
newDate.year,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200) ,
);
} }
if (selectedMonth != newDate.month) { if (selectedMonth != newDate.month) {
monthController.animateToItem( _animateColumnControllerToItem(monthController, newDate.month - 1);
newDate.month - 1,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200) ,
);
} }
if (selectedDay != newDate.day) { if (selectedDay != newDate.day) {
dayController.animateToItem( _animateColumnControllerToItem(dayController, newDate.day - 1);
newDate.day - 1,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 200) ,
);
} }
}); });
} }
......
...@@ -661,7 +661,7 @@ void main() { ...@@ -661,7 +661,7 @@ void main() {
}); });
testWidgets( testWidgets(
'picker automatically scrolls away from invalid date, ' 'date picker automatically scrolls away from invalid date, '
"and onDateTimeChanged doesn't report these dates", "and onDateTimeChanged doesn't report these dates",
(WidgetTester tester) async { (WidgetTester tester) async {
DateTime date; DateTime date;
...@@ -727,6 +727,160 @@ void main() { ...@@ -727,6 +727,160 @@ void main() {
); );
}); });
testWidgets(
'dateTime picker automatically scrolls away from invalid date, '
"and onDateTimeChanged doesn't report these dates",
(WidgetTester tester) async {
DateTime date;
final DateTime minimum = DateTime(2019, 11, 11, 3, 30);
final DateTime maximum = DateTime(2019, 11, 11, 14, 59, 59);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
minimumDate: minimum,
maximumDate: maximum,
onDateTimeChanged: (DateTime newDate) {
date = newDate;
// Callback doesn't transiently go into invalid dates.
expect(minimum.isAfter(newDate), isFalse);
expect(maximum.isBefore(newDate), isFalse);
},
initialDateTime: DateTime(2019, 11, 11, 4),
),
),
),
),
);
// 3:00 is valid but 2:00 should be invalid.
expect(
tester.widget<Text>(find.text('3')).style.color,
isNot(isSameColorAs(CupertinoColors.inactiveGray.color)),
);
expect(
tester.widget<Text>(find.text('2')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
// 'PM' is greyed out.
expect(
tester.widget<Text>(find.text('PM')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
await tester.drag(find.text('AM'), const Offset(0.0, -32.0), touchSlopY: 0.0);
await tester.pump();
await tester.pumpAndSettle(); // Now the autoscrolling should happen.
expect(
date,
DateTime(2019, 11, 11, 14, 59),
);
// 3'o clock and 'AM' are now greyed out.
expect(
tester.widget<Text>(find.text('AM')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
expect(
tester.widget<Text>(find.text('3')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
await tester.drag(find.text('PM'), const Offset(0.0, 32.0), touchSlopY: 0.0);
await tester.pump(); // Once to trigger the post frame animate call.
await tester.pumpAndSettle();
// Returns to min date.
expect(
date,
DateTime(2019, 11, 11, 3, 30),
);
});
testWidgets(
'time picker automatically scrolls away from invalid date, '
"and onDateTimeChanged doesn't report these dates",
(WidgetTester tester) async {
DateTime date;
final DateTime minimum = DateTime(2019, 11, 11, 3, 30);
final DateTime maximum = DateTime(2019, 11, 11, 14, 59, 59);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: SizedBox(
height: 400.0,
width: 400.0,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.time,
minimumDate: minimum,
maximumDate: maximum,
onDateTimeChanged: (DateTime newDate) {
date = newDate;
// Callback doesn't transiently go into invalid dates.
expect(minimum.isAfter(newDate), isFalse);
expect(maximum.isBefore(newDate), isFalse);
},
initialDateTime: DateTime(2019, 11, 11, 4),
),
),
),
),
);
// 3:00 is valid but 2:00 should be invalid.
expect(
tester.widget<Text>(find.text('3')).style.color,
isNot(isSameColorAs(CupertinoColors.inactiveGray.color)),
);
expect(
tester.widget<Text>(find.text('2')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
// 'PM' is greyed out.
expect(
tester.widget<Text>(find.text('PM')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
await tester.drag(find.text('AM'), const Offset(0.0, -32.0), touchSlopY: 0.0);
await tester.pump();
await tester.pumpAndSettle(); // Now the autoscrolling should happen.
expect(
date,
DateTime(2019, 11, 11, 14, 59),
);
// 3'o clock and 'AM' are now greyed out.
expect(
tester.widget<Text>(find.text('AM')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
expect(
tester.widget<Text>(find.text('3')).style.color,
isSameColorAs(CupertinoColors.inactiveGray.color),
);
await tester.drag(find.text('PM'), const Offset(0.0, 32.0), touchSlopY: 0.0);
await tester.pump(); // Once to trigger the post frame animate call.
await tester.pumpAndSettle();
// Returns to min date.
expect(
date,
DateTime(2019, 11, 11, 3, 30),
);
});
testWidgets('picker automatically scrolls away from invalid date on day change', (WidgetTester tester) async { testWidgets('picker automatically scrolls away from invalid date on day change', (WidgetTester tester) async {
DateTime date; DateTime date;
await tester.pumpWidget( await tester.pumpWidget(
......
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