From f4b0ccd9fd8b39922eea3458be14e80de5214424 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 1 Nov 2017 14:52:28 -0700 Subject: [PATCH] Use alwaysUse24HourFormat when formatting time of day (#12517) * alwaysUse24HourFormat in MediaQuery and time picker * docs; dead code * address some comments * MaterialLocalizations.timeOfDayFormat is the single source of 24-hour-formattedness * Make TimePickerDialog private again * wire up MediaQueryData.fromWindow to Window --- .../src/material/material_localizations.dart | 66 ++++- packages/flutter/lib/src/material/time.dart | 6 +- .../flutter/lib/src/material/time_picker.dart | 250 ++++++++++-------- .../flutter/lib/src/widgets/media_query.dart | 19 +- .../test/material/time_picker_test.dart | 80 +++++- packages/flutter/test/material/time_test.dart | 29 ++ .../lib/src/material_localizations.dart | 46 +++- .../test/date_time_test.dart | 121 +++++---- .../test/time_picker_test.dart | 83 +++++- 9 files changed, 502 insertions(+), 198 deletions(-) create mode 100644 packages/flutter/test/material/time_test.dart diff --git a/packages/flutter/lib/src/material/material_localizations.dart b/packages/flutter/lib/src/material/material_localizations.dart index a27674566b..0b994a8d86 100644 --- a/packages/flutter/lib/src/material/material_localizations.dart +++ b/packages/flutter/lib/src/material/material_localizations.dart @@ -95,7 +95,7 @@ abstract class MaterialLocalizations { /// /// The documentation for [TimeOfDayFormat] enum values provides details on /// each supported layout. - TimeOfDayFormat get timeOfDayFormat; + TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }); /// Provides geometric text preferences for the current locale. /// @@ -120,14 +120,22 @@ abstract class MaterialLocalizations { /// Formats [TimeOfDay.hour] in the given time of day according to the value /// of [timeOfDayFormat]. - String formatHour(TimeOfDay timeOfDay); + /// + /// If [alwaysUse24HourFormat] is true, formats hour using [HourFormat.HH] + /// rather than the default for the current locale. + String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }); /// Formats [TimeOfDay.minute] in the given time of day according to the value /// of [timeOfDayFormat]. String formatMinute(TimeOfDay timeOfDay); /// Formats [timeOfDay] according to the value of [timeOfDayFormat]. - String formatTimeOfDay(TimeOfDay timeOfDay); + /// + /// If [alwaysUse24HourFormat] is true, formats hour using [HourFormat.HH] + /// rather than the default for the current locale. This value is usually + /// passed from [MediaQueryData.alwaysUse24HourFormat], which has platform- + /// specific behavior. + String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }); /// Full unabbreviated year format, e.g. 2017 rather than 17. String formatYear(DateTime date); @@ -274,9 +282,27 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ]; @override - String formatHour(TimeOfDay timeOfDay) { - assert(hourFormat(of: timeOfDayFormat) == HourFormat.h); - return formatDecimal(timeOfDay.hour); + String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) { + final TimeOfDayFormat format = timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat); + switch (format) { + case TimeOfDayFormat.h_colon_mm_space_a: + return formatDecimal(timeOfDay.hourOfPeriod == 0 ? 12 : timeOfDay.hourOfPeriod); + case TimeOfDayFormat.HH_colon_mm: + return _formatTwoDigitZeroPad(timeOfDay.hour); + default: + throw new AssertionError('$runtimeType does not support $format.'); + } + } + + /// Formats [number] using two digits, assuming it's in the 0-99 inclusive + /// range. Not designed to format values outside this range. + String _formatTwoDigitZeroPad(int number) { + assert(0 <= number && number < 100); + + if (number < 10) + return '0$number'; + + return '$number'; } @override @@ -335,8 +361,7 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { } @override - String formatTimeOfDay(TimeOfDay timeOfDay) { - assert(timeOfDayFormat == TimeOfDayFormat.h_colon_mm_space_a); + String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) { // Not using intl.DateFormat for two reasons: // // - DateFormat supports more formats than our material time picker does, @@ -345,7 +370,24 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { // - DateFormat operates on DateTime, which is sensitive to time eras and // time zones, while here we want to format hour and minute within one day // no matter what date the day falls on. - return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)} ${_formatDayPeriod(timeOfDay)}'; + final StringBuffer buffer = new StringBuffer(); + + // Add hour:minute. + buffer + ..write(formatHour(timeOfDay, alwaysUse24HourFormat: alwaysUse24HourFormat)) + ..write(':') + ..write(formatMinute(timeOfDay)); + + if (alwaysUse24HourFormat) { + // There's no AM/PM indicator in 24-hour format. + return '$buffer'; + } + + // Add AM/PM indicator. + buffer + ..write(' ') + ..write(_formatDayPeriod(timeOfDay)); + return '$buffer'; } @override @@ -434,7 +476,11 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { String get postMeridiemAbbreviation => 'PM'; @override - TimeOfDayFormat get timeOfDayFormat => TimeOfDayFormat.h_colon_mm_space_a; + TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) { + return alwaysUse24HourFormat + ? TimeOfDayFormat.HH_colon_mm + : TimeOfDayFormat.h_colon_mm_space_a; + } /// Looks up text geometry defined in [MaterialTextGeometry]. @override diff --git a/packages/flutter/lib/src/material/time.dart b/packages/flutter/lib/src/material/time.dart index 375e39f6e8..0d2da35ac4 100644 --- a/packages/flutter/lib/src/material/time.dart +++ b/packages/flutter/lib/src/material/time.dart @@ -84,8 +84,12 @@ class TimeOfDay { /// /// This is a shortcut for [MaterialLocalizations.formatTimeOfDay]. String format(BuildContext context) { + debugCheckHasMediaQuery(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context); - return localizations.formatTimeOfDay(this); + return localizations.formatTimeOfDay( + this, + alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat, + ); } @override diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index ebebe37ebf..7760f18fab 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -224,14 +224,13 @@ class _DayPeriodControl extends StatelessWidget { class _HourControl extends StatelessWidget { const _HourControl({ @required this.fragmentContext, - @required this.hourFormat, }); final _TimePickerFragmentContext fragmentContext; - final HourFormat hourFormat; @override Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final TextStyle hourStyle = fragmentContext.mode == _TimePickerMode.hour ? fragmentContext.activeStyle @@ -239,7 +238,10 @@ class _HourControl extends StatelessWidget { return new GestureDetector( onTap: Feedback.wrapForTap(() => fragmentContext.onModeChange(_TimePickerMode.hour), context), - child: new Text(localizations.formatHour(fragmentContext.selectedTime), style: hourStyle), + child: new Text(localizations.formatHour( + fragmentContext.selectedTime, + alwaysUse24HourFormat: MediaQuery.of(context).alwaysUse24HourFormat, + ), style: hourStyle), ); } } @@ -285,15 +287,16 @@ class _MinuteControl extends StatelessWidget { } /// Provides time picker header layout configuration for the given -/// [timeOfDayFormat] passing [context] to each widget in the configuration. +/// [timeOfDayFormat] passing [context] to each widget in the +/// configuration. /// /// The [timeOfDayFormat] and [context] arguments must not be null. _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _TimePickerFragmentContext context) { // Creates an hour fragment. - _TimePickerHeaderFragment hour(HourFormat hourFormat) { + _TimePickerHeaderFragment hour() { return new _TimePickerHeaderFragment( layoutId: _TimePickerHeaderId.hour, - widget: new _HourControl(fragmentContext: context, hourFormat: hourFormat), + widget: new _HourControl(fragmentContext: context), startMargin: _kPeriodGap, ); } @@ -327,7 +330,7 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim } // Convenience function for creating a time header format with up to two pieces. - _TimePickerHeaderFormat format(int centrepieceIndex, _TimePickerHeaderPiece piece1, + _TimePickerHeaderFormat format(_TimePickerHeaderPiece piece1, [ _TimePickerHeaderPiece piece2 ]) { final List<_TimePickerHeaderPiece> pieces = <_TimePickerHeaderPiece>[]; switch (context.textDirection) { @@ -340,9 +343,15 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim if (piece2 != null) pieces.add(piece2); pieces.add(piece1); - centrepieceIndex = pieces.length - centrepieceIndex - 1; break; } + int centrepieceIndex; + for (int i = 0; i < pieces.length; i += 1) { + if (pieces[i].pivotIndex >= 0) { + centrepieceIndex = i; + } + } + assert(centrepieceIndex != null); return new _TimePickerHeaderFormat(centrepieceIndex, pieces); } @@ -361,10 +370,9 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim switch (timeOfDayFormat) { case TimeOfDayFormat.h_colon_mm_space_a: return format( - 0, piece( pivotIndex: 1, - fragment1: hour(HourFormat.h), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.colon, ':'), fragment3: minute(), ), @@ -374,44 +382,43 @@ _TimePickerHeaderFormat _buildHeaderFormat(TimeOfDayFormat timeOfDayFormat, _Tim ), ); case TimeOfDayFormat.H_colon_mm: - return format(0, piece( + return format(piece( pivotIndex: 1, - fragment1: hour(HourFormat.H), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.colon, ':'), fragment3: minute(), )); case TimeOfDayFormat.HH_dot_mm: - return format(0, piece( + return format(piece( pivotIndex: 1, - fragment1: hour(HourFormat.HH), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.dot, '.'), fragment3: minute(), )); case TimeOfDayFormat.a_space_h_colon_mm: return format( - 1, piece( bottomMargin: _kVerticalGap, fragment1: dayPeriod(), ), piece( pivotIndex: 1, - fragment1: hour(HourFormat.h), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.colon, ':'), fragment3: minute(), ), ); case TimeOfDayFormat.frenchCanadian: - return format(0, piece( + return format(piece( pivotIndex: 1, - fragment1: hour(HourFormat.HH), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.hString, 'h'), fragment3: minute(), )); case TimeOfDayFormat.HH_colon_mm: - return format(0, piece( + return format(piece( pivotIndex: 1, - fragment1: hour(HourFormat.HH), + fragment1: hour(), fragment2: string(_TimePickerHeaderId.colon, ':'), fragment3: minute(), )); @@ -571,8 +578,11 @@ class _TimePickerHeader extends StatelessWidget { @override Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); final ThemeData themeData = Theme.of(context); - final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat; + final MediaQueryData media = MediaQuery.of(context); + final TimeOfDayFormat timeOfDayFormat = MaterialLocalizations.of(context) + .timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat); EdgeInsets padding; double height; @@ -651,7 +661,7 @@ class _TimePickerHeader extends StatelessWidget { } } -List _initPainters(TextTheme textTheme, List labels) { +List _buildPainters(TextTheme textTheme, List labels) { final TextStyle style = textTheme.subhead; final List painters = new List(labels.length); for (int i = 0; i < painters.length; ++i) { @@ -671,67 +681,6 @@ enum _DialRing { inner, } -const List _amHours = const [ - const TimeOfDay(hour: 0, minute: 0), - const TimeOfDay(hour: 1, minute: 0), - const TimeOfDay(hour: 2, minute: 0), - const TimeOfDay(hour: 3, minute: 0), - const TimeOfDay(hour: 4, minute: 0), - const TimeOfDay(hour: 5, minute: 0), - const TimeOfDay(hour: 6, minute: 0), - const TimeOfDay(hour: 7, minute: 0), - const TimeOfDay(hour: 8, minute: 0), - const TimeOfDay(hour: 9, minute: 0), - const TimeOfDay(hour: 10, minute: 0), - const TimeOfDay(hour: 11, minute: 0), -]; - -const List _pmHours = const [ - const TimeOfDay(hour: 12, minute: 0), - const TimeOfDay(hour: 13, minute: 0), - const TimeOfDay(hour: 14, minute: 0), - const TimeOfDay(hour: 15, minute: 0), - const TimeOfDay(hour: 16, minute: 0), - const TimeOfDay(hour: 17, minute: 0), - const TimeOfDay(hour: 18, minute: 0), - const TimeOfDay(hour: 19, minute: 0), - const TimeOfDay(hour: 20, minute: 0), - const TimeOfDay(hour: 21, minute: 0), - const TimeOfDay(hour: 22, minute: 0), - const TimeOfDay(hour: 23, minute: 0), -]; - -List _init24HourInnerRing(TextTheme textTheme, MaterialLocalizations localizations) { - return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList()); -} - -List _init24HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) { - return _initPainters(textTheme, _pmHours.map(localizations.formatHour).toList()); -} - -List _init12HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) { - return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList()); -} - -const List _minuteMarkerValues = const [ - const TimeOfDay(hour: 0, minute: 0), - const TimeOfDay(hour: 0, minute: 5), - const TimeOfDay(hour: 0, minute: 10), - const TimeOfDay(hour: 0, minute: 15), - const TimeOfDay(hour: 0, minute: 20), - const TimeOfDay(hour: 0, minute: 25), - const TimeOfDay(hour: 0, minute: 30), - const TimeOfDay(hour: 0, minute: 35), - const TimeOfDay(hour: 0, minute: 40), - const TimeOfDay(hour: 0, minute: 45), - const TimeOfDay(hour: 0, minute: 50), - const TimeOfDay(hour: 0, minute: 55), -]; - -List _initMinutes(TextTheme textTheme, MaterialLocalizations localizations) { - return _initPainters(textTheme, _minuteMarkerValues.map(localizations.formatMinute).toList()); -} - class _DialPainter extends CustomPainter { const _DialPainter({ @required this.primaryOuterLabels, @@ -830,13 +779,13 @@ class _Dial extends StatefulWidget { const _Dial({ @required this.selectedTime, @required this.mode, - @required this.is24h, + @required this.use24HourDials, @required this.onChanged }) : assert(selectedTime != null); final TimeOfDay selectedTime; final _TimePickerMode mode; - final bool is24h; + final bool use24HourDials; final ValueChanged onChanged; @override @@ -858,6 +807,19 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { ))..addListener(() => setState(() { })); } + ThemeData themeData; + MaterialLocalizations localizations; + MediaQueryData media; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + assert(debugCheckHasMediaQuery(context)); + themeData = Theme.of(context); + localizations = MaterialLocalizations.of(context); + media = MediaQuery.of(context); + } + @override void didUpdateWidget(_Dial oldWidget) { super.didUpdateWidget(oldWidget); @@ -865,7 +827,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { if (!_dragging) _animateTo(_getThetaForTime(widget.selectedTime)); } - if (widget.mode == _TimePickerMode.hour && widget.is24h && widget.selectedTime.period == DayPeriod.am) { + if (widget.mode == _TimePickerMode.hour && widget.use24HourDials && widget.selectedTime.period == DayPeriod.am) { _activeRing = _DialRing.inner; } else { _activeRing = _DialRing.outer; @@ -910,7 +872,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { final double fraction = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0; if (widget.mode == _TimePickerMode.hour) { int newHour = (fraction * TimeOfDay.hoursPerPeriod).round() % TimeOfDay.hoursPerPeriod; - if (widget.is24h) { + if (widget.use24HourDials) { if (_activeRing == _DialRing.outer) { if (newHour != 0) newHour = (newHour + TimeOfDay.hoursPerPeriod) % TimeOfDay.hoursPerDay; @@ -945,7 +907,7 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { ..end = angle; // The controller doesn't animate during the pan gesture. final RenderBox box = context.findRenderObject(); final double radius = box.size.shortestSide / 2.0; - if (widget.mode == _TimePickerMode.hour && widget.is24h) { + if (widget.mode == _TimePickerMode.hour && widget.use24HourDials) { if (offset.distance * 1.5 < radius) _activeRing = _DialRing.inner; else @@ -982,11 +944,81 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { _animateTo(_getThetaForTime(widget.selectedTime)); } + static const List _amHours = const [ + const TimeOfDay(hour: 12, minute: 0), + const TimeOfDay(hour: 1, minute: 0), + const TimeOfDay(hour: 2, minute: 0), + const TimeOfDay(hour: 3, minute: 0), + const TimeOfDay(hour: 4, minute: 0), + const TimeOfDay(hour: 5, minute: 0), + const TimeOfDay(hour: 6, minute: 0), + const TimeOfDay(hour: 7, minute: 0), + const TimeOfDay(hour: 8, minute: 0), + const TimeOfDay(hour: 9, minute: 0), + const TimeOfDay(hour: 10, minute: 0), + const TimeOfDay(hour: 11, minute: 0), + ]; + + static const List _pmHours = const [ + const TimeOfDay(hour: 0, minute: 0), + const TimeOfDay(hour: 13, minute: 0), + const TimeOfDay(hour: 14, minute: 0), + const TimeOfDay(hour: 15, minute: 0), + const TimeOfDay(hour: 16, minute: 0), + const TimeOfDay(hour: 17, minute: 0), + const TimeOfDay(hour: 18, minute: 0), + const TimeOfDay(hour: 19, minute: 0), + const TimeOfDay(hour: 20, minute: 0), + const TimeOfDay(hour: 21, minute: 0), + const TimeOfDay(hour: 22, minute: 0), + const TimeOfDay(hour: 23, minute: 0), + ]; + + List _build24HourInnerRing(TextTheme textTheme) { + return _buildPainters(textTheme, _amHours + .map((TimeOfDay timeOfDay) { + return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat); + }) + .toList()); + } + + List _build24HourOuterRing(TextTheme textTheme) { + return _buildPainters(textTheme, _pmHours + .map((TimeOfDay timeOfDay) { + return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat); + }) + .toList()); + } + + List _build12HourOuterRing(TextTheme textTheme) { + return _buildPainters(textTheme, _amHours + .map((TimeOfDay timeOfDay) { + return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat); + }) + .toList()); + } + + List _buildMinutes(TextTheme textTheme) { + const List _minuteMarkerValues = const [ + const TimeOfDay(hour: 0, minute: 0), + const TimeOfDay(hour: 0, minute: 5), + const TimeOfDay(hour: 0, minute: 10), + const TimeOfDay(hour: 0, minute: 15), + const TimeOfDay(hour: 0, minute: 20), + const TimeOfDay(hour: 0, minute: 25), + const TimeOfDay(hour: 0, minute: 30), + const TimeOfDay(hour: 0, minute: 35), + const TimeOfDay(hour: 0, minute: 40), + const TimeOfDay(hour: 0, minute: 45), + const TimeOfDay(hour: 0, minute: 50), + const TimeOfDay(hour: 0, minute: 55), + ]; + + return _buildPainters(textTheme, _minuteMarkerValues.map(localizations.formatMinute).toList()); + } + @override Widget build(BuildContext context) { - final ThemeData themeData = Theme.of(context); - final MaterialLocalizations localizations = MaterialLocalizations.of(context); - Color backgroundColor; switch (themeData.brightness) { case Brightness.light: @@ -1004,20 +1036,20 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { List secondaryInnerLabels; switch (widget.mode) { case _TimePickerMode.hour: - if (widget.is24h) { - primaryOuterLabels = _init24HourOuterRing(theme.textTheme, localizations); - secondaryOuterLabels = _init24HourOuterRing(theme.accentTextTheme, localizations); - primaryInnerLabels = _init24HourInnerRing(theme.textTheme, localizations); - secondaryInnerLabels = _init24HourInnerRing(theme.accentTextTheme, localizations); + if (widget.use24HourDials) { + primaryOuterLabels = _build24HourOuterRing(theme.textTheme); + secondaryOuterLabels = _build24HourOuterRing(theme.accentTextTheme); + primaryInnerLabels = _build24HourInnerRing(theme.textTheme); + secondaryInnerLabels = _build24HourInnerRing(theme.accentTextTheme); } else { - primaryOuterLabels = _init12HourOuterRing(theme.textTheme, localizations); - secondaryOuterLabels = _init12HourOuterRing(theme.accentTextTheme, localizations); + primaryOuterLabels = _build12HourOuterRing(theme.textTheme); + secondaryOuterLabels = _build12HourOuterRing(theme.accentTextTheme); } break; case _TimePickerMode.minute: - primaryOuterLabels = _initMinutes(theme.textTheme, localizations); + primaryOuterLabels = _buildMinutes(theme.textTheme); primaryInnerLabels = null; - secondaryOuterLabels = _initMinutes(theme.accentTextTheme, localizations); + secondaryOuterLabels = _buildMinutes(theme.accentTextTheme); secondaryInnerLabels = null; break; } @@ -1043,13 +1075,23 @@ class _DialState extends State<_Dial> with SingleTickerProviderStateMixin { } } +/// A material design time picker designed to appear inside a popup dialog. +/// +/// Pass this widget to [showDialog]. The value returned by [showDialog] is the +/// selected [TimeOfDay] if the user taps the "OK" button, or null if the user +/// taps the "CANCEL" button. The selected time is reported by calling +/// [Navigator.pop]. class _TimePickerDialog extends StatefulWidget { + /// Creates a material time picker. + /// + /// [initialTime] must not be null. const _TimePickerDialog({ Key key, @required this.initialTime }) : assert(initialTime != null), super(key: key); + /// The time initially selected when the dialog is shown. final TimeOfDay initialTime; @override @@ -1106,8 +1148,10 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { @override Widget build(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); final MaterialLocalizations localizations = MaterialLocalizations.of(context); - final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat; + final MediaQueryData media = MediaQuery.of(context); + final TimeOfDayFormat timeOfDayFormat = localizations.timeOfDayFormat(alwaysUse24HourFormat: media.alwaysUse24HourFormat); final Widget picker = new Padding( padding: const EdgeInsets.all(16.0), @@ -1115,7 +1159,7 @@ class _TimePickerDialogState extends State<_TimePickerDialog> { aspectRatio: 1.0, child: new _Dial( mode: _mode, - is24h: hourFormat(of: timeOfDayFormat) != HourFormat.h, + use24HourDials: hourFormat(of: timeOfDayFormat) != HourFormat.h, selectedTime: _selectedTime, onChanged: _handleTimeChanged, ) @@ -1222,7 +1266,7 @@ Future showTimePicker({ }) async { assert(context != null); assert(initialTime != null); - return await showDialog( + return await showDialog( context: context, child: new _TimePickerDialog(initialTime: initialTime), ); diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index f29e81499d..35474b9b11 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -40,7 +40,8 @@ class MediaQueryData { this.size: Size.zero, this.devicePixelRatio: 1.0, this.textScaleFactor: 1.0, - this.padding: EdgeInsets.zero + this.padding: EdgeInsets.zero, + this.alwaysUse24HourFormat: false, }); /// Creates data for a media query based on the given window. @@ -53,7 +54,8 @@ class MediaQueryData { : size = window.physicalSize / window.devicePixelRatio, devicePixelRatio = window.devicePixelRatio, textScaleFactor = window.textScaleFactor, - padding = new EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio); + padding = new EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio), + alwaysUse24HourFormat = window.alwaysUse24HourFormat; /// The size of the media in logical pixel (e.g, the size of the screen). /// @@ -77,6 +79,19 @@ class MediaQueryData { /// The padding around the edges of the media (e.g., the screen). final EdgeInsets padding; + /// Whether to use 24-hour format when formatting time. + /// + /// The behavior of this flag is different across platforms: + /// + /// - On Android this flag is reported directly from the user settings called + /// "Use 24-hour format". It applies to any locale used by the application, + /// whether it is the system-wide locale, or the custom locale set by the + /// application. + /// - On iOS this flag is set to true when the user setting called "24-Hour + /// Time" is set or the system-wide locale's default uses 24-hour + /// formatting. + final bool alwaysUse24HourFormat; + /// The orientation of the media (e.g., whether the device is in landscape or portrait mode). Orientation get orientation { return size.width > size.height ? Orientation.landscape : Orientation.portrait; diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 863dcef660..7e7ad2ed9f 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -29,7 +31,7 @@ class _TimePickerLauncher extends StatelessWidget { onPressed: () async { onChanged(await showTimePicker( context: context, - initialTime: const TimeOfDay(hour: 7, minute: 0) + initialTime: const TimeOfDay(hour: 7, minute: 0), )); } ); @@ -41,17 +43,15 @@ class _TimePickerLauncher extends StatelessWidget { } } -Future startPicker(WidgetTester tester, ValueChanged onChanged, - { Locale locale: const Locale('en', 'US') }) async { - await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: locale,)); +Future startPicker(WidgetTester tester, ValueChanged onChanged) async { + await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US'))); await tester.tap(find.text('X')); await tester.pumpAndSettle(const Duration(seconds: 1)); return tester.getCenter(find.byKey(const Key('time-picker-dial'))); } Future finishPicker(WidgetTester tester) async { - final Element timePickerElement = tester.element(find.byElementPredicate((Element element) => element.widget.runtimeType.toString() == '_TimePickerDialog')); - final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(timePickerElement); + final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(RaisedButton))); await tester.tap(find.text(materialLocalizations.okButtonLabel)); await tester.pumpAndSettle(const Duration(seconds: 1)); } @@ -205,4 +205,72 @@ void main() { expect(feedback.hapticCount, 3); }); }); + + const List labels12To11 = const ['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; + const List labels12To11TwoDigit = const ['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11']; + const List labels00To23 = const ['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; + + Future mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async { + await tester.pumpWidget( + new Localizations( + locale: const Locale('en', 'US'), + delegates: >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: new MediaQuery( + data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Navigator( + onGenerateRoute: (RouteSettings settings) { + return new MaterialPageRoute(builder: (BuildContext context) { + showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); + return new Container(); + }); + }, + ), + ), + ), + ), + ); + // Pump once, because the dialog shows up asynchronously. + await tester.pump(); + } + + testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false); + + final CustomPaint dialPaint = tester.widget(find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'), + matching: find.byType(CustomPaint), + )); + final dynamic dialPainter = dialPaint.painter; + final List primaryOuterLabels = dialPainter.primaryOuterLabels; + expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11); + expect(dialPainter.primaryInnerLabels, null); + + final List secondaryOuterLabels = dialPainter.secondaryOuterLabels; + expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11); + expect(dialPainter.secondaryInnerLabels, null); + }); + + testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, true); + + final CustomPaint dialPaint = tester.widget(find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'), + matching: find.byType(CustomPaint), + )); + final dynamic dialPainter = dialPaint.painter; + final List primaryOuterLabels = dialPainter.primaryOuterLabels; + expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23); + final List primaryInnerLabels = dialPainter.primaryInnerLabels; + expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit); + + final List secondaryOuterLabels = dialPainter.secondaryOuterLabels; + expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23); + final List secondaryInnerLabels = dialPainter.secondaryInnerLabels; + expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit); + }); } diff --git a/packages/flutter/test/material/time_test.dart b/packages/flutter/test/material/time_test.dart new file mode 100644 index 0000000000..25bf7fe709 --- /dev/null +++ b/packages/flutter/test/material/time_test.dart @@ -0,0 +1,29 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('TimeOfDay.format', () { + testWidgets('respects alwaysUse24HourFormat option', (WidgetTester tester) async { + Future pumpTest(bool alwaysUse24HourFormat) async { + String formattedValue; + await tester.pumpWidget(new MaterialApp( + home: new MediaQuery( + data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat), + child: new Builder(builder: (BuildContext context) { + formattedValue = const TimeOfDay(hour: 7, minute: 0).format(context); + return new Container(); + }), + ), + )); + return formattedValue; + } + + expect(await pumpTest(false), '7:00 AM'); + expect(await pumpTest(true), '07:00'); + }); + }); +} diff --git a/packages/flutter_localizations/lib/src/material_localizations.dart b/packages/flutter_localizations/lib/src/material_localizations.dart index 4825451f09..e1fb7d2fb2 100644 --- a/packages/flutter_localizations/lib/src/material_localizations.dart +++ b/packages/flutter_localizations/lib/src/material_localizations.dart @@ -141,8 +141,8 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { } @override - String formatHour(TimeOfDay timeOfDay) { - switch (hourFormat(of: timeOfDayFormat)) { + String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) { + switch (hourFormat(of: timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat))) { case HourFormat.HH: return _twoDigitZeroPaddedFormat.format(timeOfDay.hour); case HourFormat.H: @@ -188,7 +188,7 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { } @override - String formatTimeOfDay(TimeOfDay timeOfDay) { + String formatTimeOfDay(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat: false }) { // Not using intl.DateFormat for two reasons: // // - DateFormat supports more formats than our material time picker does, @@ -197,18 +197,20 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { // - DateFormat operates on DateTime, which is sensitive to time eras and // time zones, while here we want to format hour and minute within one day // no matter what date the day falls on. - switch (timeOfDayFormat) { + final String hour = formatHour(timeOfDay, alwaysUse24HourFormat: alwaysUse24HourFormat); + final String minute = formatMinute(timeOfDay); + switch (timeOfDayFormat(alwaysUse24HourFormat: alwaysUse24HourFormat)) { case TimeOfDayFormat.h_colon_mm_space_a: - return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)} ${_formatDayPeriod(timeOfDay)}'; + return '$hour:$minute ${_formatDayPeriod(timeOfDay)}'; case TimeOfDayFormat.H_colon_mm: case TimeOfDayFormat.HH_colon_mm: - return '${formatHour(timeOfDay)}:${formatMinute(timeOfDay)}'; + return '$hour:$minute'; case TimeOfDayFormat.HH_dot_mm: - return '${formatHour(timeOfDay)}.${formatMinute(timeOfDay)}'; + return '$hour.$minute'; case TimeOfDayFormat.a_space_h_colon_mm: - return '${_formatDayPeriod(timeOfDay)} ${formatHour(timeOfDay)}:${formatMinute(timeOfDay)}'; + return '${_formatDayPeriod(timeOfDay)} $hour:$minute'; case TimeOfDayFormat.frenchCanadian: - return '${formatHour(timeOfDay)} h ${formatMinute(timeOfDay)}'; + return '$hour h $minute'; } return null; @@ -328,7 +330,7 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { /// * http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US shows the /// short time pattern used in locale en_US @override - TimeOfDayFormat get timeOfDayFormat { + TimeOfDayFormat timeOfDayFormat({ bool alwaysUse24HourFormat: false }) { final String icuShortTimePattern = _nameToValue['timeOfDayFormat']; assert(() { @@ -343,7 +345,12 @@ class GlobalMaterialLocalizations implements MaterialLocalizations { return true; }()); - return _icuTimeOfDayToEnum[icuShortTimePattern]; + final TimeOfDayFormat icuFormat = _icuTimeOfDayToEnum[icuShortTimePattern]; + + if (alwaysUse24HourFormat) + return _get24HourVersionOf(icuFormat); + + return icuFormat; } /// Looks up text geometry defined in [MaterialTextGeometry]. @@ -403,6 +410,23 @@ const Map _icuTimeOfDayToEnum = const formatHour(WidgetTester tester, Locale locale, TimeOfDay timeOfDay) async { + final Completer completer = new Completer(); + await tester.pumpWidget(new MaterialApp( + supportedLocales: [locale], + locale: locale, + localizationsDelegates: >[ + GlobalMaterialLocalizations.delegate, + ], + home: new Builder(builder: (BuildContext context) { + completer.complete(MaterialLocalizations.of(context).formatHour(timeOfDay)); + return new Container(); + }), + )); + return completer.future; + } - localizations = new GlobalMaterialLocalizations(const Locale('en', 'US')); - expect(localizations.formatHour(const TimeOfDay(hour: 10, minute: 0)), '10'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '8'); + testWidgets('formats h', (WidgetTester tester) async { + expect(await formatHour(tester, const Locale('en', 'US'), const TimeOfDay(hour: 10, minute: 0)), '10'); + expect(await formatHour(tester, const Locale('en', 'US'), const TimeOfDay(hour: 20, minute: 0)), '8'); - localizations = new GlobalMaterialLocalizations(const Locale('ar', '')); - expect(localizations.formatHour(const TimeOfDay(hour: 10, minute: 0)), '١٠'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '٨'); + expect(await formatHour(tester, const Locale('ar', ''), const TimeOfDay(hour: 10, minute: 0)), '١٠'); + expect(await formatHour(tester, const Locale('ar', ''), const TimeOfDay(hour: 20, minute: 0)), '٨'); }); - test('formats HH', () { - GlobalMaterialLocalizations localizations; + testWidgets('formats HH', (WidgetTester tester) async { + expect(await formatHour(tester, const Locale('de', ''), const TimeOfDay(hour: 9, minute: 0)), '09'); + expect(await formatHour(tester, const Locale('de', ''), const TimeOfDay(hour: 20, minute: 0)), '20'); - localizations = new GlobalMaterialLocalizations(const Locale('de', '')); - expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '09'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20'); - - localizations = new GlobalMaterialLocalizations(const Locale('en', 'GB')); - expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '09'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20'); + expect(await formatHour(tester, const Locale('en', 'GB'), const TimeOfDay(hour: 9, minute: 0)), '09'); + expect(await formatHour(tester, const Locale('en', 'GB'), const TimeOfDay(hour: 20, minute: 0)), '20'); }); - test('formats H', () { - GlobalMaterialLocalizations localizations; + testWidgets('formats H', (WidgetTester tester) async { + expect(await formatHour(tester, const Locale('es', ''), const TimeOfDay(hour: 9, minute: 0)), '9'); + expect(await formatHour(tester, const Locale('es', ''), const TimeOfDay(hour: 20, minute: 0)), '20'); - localizations = new GlobalMaterialLocalizations(const Locale('es', '')); - expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '9'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '20'); - - localizations = new GlobalMaterialLocalizations(const Locale('fa', '')); - expect(localizations.formatHour(const TimeOfDay(hour: 9, minute: 0)), '۹'); - expect(localizations.formatHour(const TimeOfDay(hour: 20, minute: 0)), '۲۰'); + expect(await formatHour(tester, const Locale('fa', ''), const TimeOfDay(hour: 9, minute: 0)), '۹'); + expect(await formatHour(tester, const Locale('fa', ''), const TimeOfDay(hour: 20, minute: 0)), '۲۰'); }); }); @@ -74,48 +80,49 @@ void main() { }); group('formatTimeOfDay', () { - test('formats ${TimeOfDayFormat.h_colon_mm_space_a}', () { - GlobalMaterialLocalizations localizations; + Future formatTimeOfDay(WidgetTester tester, Locale locale, TimeOfDay timeOfDay) async { + final Completer completer = new Completer(); + await tester.pumpWidget(new MaterialApp( + supportedLocales: [locale], + locale: locale, + localizationsDelegates: >[ + GlobalMaterialLocalizations.delegate, + ], + home: new Builder(builder: (BuildContext context) { + completer.complete(MaterialLocalizations.of(context).formatTimeOfDay(timeOfDay)); + return new Container(); + }), + )); + return completer.future; + } - localizations = new GlobalMaterialLocalizations(const Locale('ar', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '٩:٣٢ ص'); + testWidgets('formats ${TimeOfDayFormat.h_colon_mm_space_a}', (WidgetTester tester) async { + expect(await formatTimeOfDay(tester, const Locale('ar', ''), const TimeOfDay(hour: 9, minute: 32)), '٩:٣٢ ص'); + expect(await formatTimeOfDay(tester, const Locale('ar', ''), const TimeOfDay(hour: 20, minute: 32)), '٨:٣٢ م'); - localizations = new GlobalMaterialLocalizations(const Locale('en', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32 AM'); + expect(await formatTimeOfDay(tester, const Locale('en', ''), const TimeOfDay(hour: 9, minute: 32)), '9:32 AM'); + expect(await formatTimeOfDay(tester, const Locale('en', ''), const TimeOfDay(hour: 20, minute: 32)), '8:32 PM'); }); - test('formats ${TimeOfDayFormat.HH_colon_mm}', () { - GlobalMaterialLocalizations localizations; - - localizations = new GlobalMaterialLocalizations(const Locale('de', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32'); - - localizations = new GlobalMaterialLocalizations(const Locale('en', 'ZA')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09:32'); + testWidgets('formats ${TimeOfDayFormat.HH_colon_mm}', (WidgetTester tester) async { + expect(await formatTimeOfDay(tester, const Locale('de', ''), const TimeOfDay(hour: 9, minute: 32)), '09:32'); + expect(await formatTimeOfDay(tester, const Locale('en', 'ZA'), const TimeOfDay(hour: 9, minute: 32)), '09:32'); }); - test('formats ${TimeOfDayFormat.H_colon_mm}', () { - GlobalMaterialLocalizations localizations; + testWidgets('formats ${TimeOfDayFormat.H_colon_mm}', (WidgetTester tester) async { + expect(await formatTimeOfDay(tester, const Locale('es', ''), const TimeOfDay(hour: 9, minute: 32)), '9:32'); + expect(await formatTimeOfDay(tester, const Locale('es', ''), const TimeOfDay(hour: 20, minute: 32)), '20:32'); - localizations = new GlobalMaterialLocalizations(const Locale('es', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32'); - - localizations = new GlobalMaterialLocalizations(const Locale('ja', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '9:32'); + expect(await formatTimeOfDay(tester, const Locale('ja', ''), const TimeOfDay(hour: 9, minute: 32)), '9:32'); + expect(await formatTimeOfDay(tester, const Locale('ja', ''), const TimeOfDay(hour: 20, minute: 32)), '20:32'); }); - test('formats ${TimeOfDayFormat.frenchCanadian}', () { - GlobalMaterialLocalizations localizations; - - localizations = new GlobalMaterialLocalizations(const Locale('fr', 'CA')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '09 h 32'); + testWidgets('formats ${TimeOfDayFormat.frenchCanadian}', (WidgetTester tester) async { + expect(await formatTimeOfDay(tester, const Locale('fr', 'CA'), const TimeOfDay(hour: 9, minute: 32)), '09 h 32'); }); - test('formats ${TimeOfDayFormat.a_space_h_colon_mm}', () { - GlobalMaterialLocalizations localizations; - - localizations = new GlobalMaterialLocalizations(const Locale('zh', '')); - expect(localizations.formatTimeOfDay(const TimeOfDay(hour: 9, minute: 32)), '上午 9:32'); + testWidgets('formats ${TimeOfDayFormat.a_space_h_colon_mm}', (WidgetTester tester) async { + expect(await formatTimeOfDay(tester, const Locale('zh', ''), const TimeOfDay(hour: 9, minute: 32)), '上午 9:32'); }); }); }); diff --git a/packages/flutter_localizations/test/time_picker_test.dart b/packages/flutter_localizations/test/time_picker_test.dart index 8e770d2f5d..8b70e49d04 100644 --- a/packages/flutter_localizations/test/time_picker_test.dart +++ b/packages/flutter_localizations/test/time_picker_test.dart @@ -48,8 +48,7 @@ Future startPicker(WidgetTester tester, ValueChanged onChange } Future finishPicker(WidgetTester tester) async { - final Element timePickerElement = tester.element(find.byElementPredicate((Element element) => element.widget.runtimeType.toString() == '_TimePickerDialog')); - final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(timePickerElement); + final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(RaisedButton))); await tester.tap(find.text(materialLocalizations.okButtonLabel)); await tester.pumpAndSettle(const Duration(seconds: 1)); } @@ -58,11 +57,11 @@ void main() { testWidgets('can localize the header in all known formats', (WidgetTester tester) async { // TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them final Map> locales = >{ - const Locale('en', 'US'): const ['hour h', 'string :', 'minute', 'period'], //'h:mm a' - const Locale('en', 'GB'): const ['hour HH', 'string :', 'minute'], //'HH:mm' - const Locale('es', 'ES'): const ['hour H', 'string :', 'minute'], //'H:mm' - const Locale('fr', 'CA'): const ['hour HH', 'string h', 'minute'], //'HH \'h\' mm' - const Locale('zh', 'ZH'): const ['period', 'hour h', 'string :', 'minute'], //'ah:mm' + const Locale('en', 'US'): const ['hour', 'string :', 'minute', 'period'], //'h:mm a' + const Locale('en', 'GB'): const ['hour', 'string :', 'minute'], //'HH:mm' + const Locale('es', 'ES'): const ['hour', 'string :', 'minute'], //'H:mm' + const Locale('fr', 'CA'): const ['hour', 'string h', 'minute'], //'HH \'h\' mm' + const Locale('zh', 'ZH'): const ['period', 'hour', 'string :', 'minute'], //'ah:mm' }; for (Locale locale in locales.keys) { @@ -77,7 +76,7 @@ void main() { } else if (fragmentType == '_DayPeriodControl') { actual.add('period'); } else if (fragmentType == '_HourControl') { - actual.add('hour ${widget.hourFormat.toString().split('.').last}'); + actual.add('hour'); } else if (fragmentType == '_StringFragment') { actual.add('string ${widget.value}'); } else { @@ -126,4 +125,72 @@ void main() { } } }); + + const List labels12To11 = const ['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; + const List labels12To11TwoDigit = const ['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11']; + const List labels00To23 = const ['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; + + Future mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async { + await tester.pumpWidget( + new Localizations( + locale: const Locale('en', 'US'), + delegates: >[ + GlobalMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: new MediaQuery( + data: new MediaQueryData(alwaysUse24HourFormat: alwaysUse24HourFormat), + child: new Directionality( + textDirection: TextDirection.ltr, + child: new Navigator( + onGenerateRoute: (RouteSettings settings) { + return new MaterialPageRoute(builder: (BuildContext context) { + showTimePicker(context: context, initialTime: const TimeOfDay(hour: 7, minute: 0)); + return new Container(); + }); + }, + ), + ), + ), + ), + ); + // Pump once, because the dialog shows up asynchronously. + await tester.pump(); + } + + testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, false); + + final CustomPaint dialPaint = tester.widget(find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'), + matching: find.byType(CustomPaint), + )); + final dynamic dialPainter = dialPaint.painter; + final List primaryOuterLabels = dialPainter.primaryOuterLabels; + expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11); + expect(dialPainter.primaryInnerLabels, null); + + final List secondaryOuterLabels = dialPainter.secondaryOuterLabels; + expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11); + expect(dialPainter.secondaryInnerLabels, null); + }); + + testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async { + await mediaQueryBoilerplate(tester, true); + + final CustomPaint dialPaint = tester.widget(find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'), + matching: find.byType(CustomPaint), + )); + final dynamic dialPainter = dialPaint.painter; + final List primaryOuterLabels = dialPainter.primaryOuterLabels; + expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23); + final List primaryInnerLabels = dialPainter.primaryInnerLabels; + expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit); + + final List secondaryOuterLabels = dialPainter.secondaryOuterLabels; + expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23); + final List secondaryInnerLabels = dialPainter.secondaryInnerLabels; + expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit); + }); }