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
This commit is contained in:
parent
82dbd128ae
commit
f4b0ccd9fd
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<TextPainter> _initPainters(TextTheme textTheme, List<String> labels) {
|
||||
List<TextPainter> _buildPainters(TextTheme textTheme, List<String> labels) {
|
||||
final TextStyle style = textTheme.subhead;
|
||||
final List<TextPainter> painters = new List<TextPainter>(labels.length);
|
||||
for (int i = 0; i < painters.length; ++i) {
|
||||
@ -671,67 +681,6 @@ enum _DialRing {
|
||||
inner,
|
||||
}
|
||||
|
||||
const List<TimeOfDay> _amHours = const <TimeOfDay>[
|
||||
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<TimeOfDay> _pmHours = const <TimeOfDay>[
|
||||
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<TextPainter> _init24HourInnerRing(TextTheme textTheme, MaterialLocalizations localizations) {
|
||||
return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList());
|
||||
}
|
||||
|
||||
List<TextPainter> _init24HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) {
|
||||
return _initPainters(textTheme, _pmHours.map(localizations.formatHour).toList());
|
||||
}
|
||||
|
||||
List<TextPainter> _init12HourOuterRing(TextTheme textTheme, MaterialLocalizations localizations) {
|
||||
return _initPainters(textTheme, _amHours.map(localizations.formatHour).toList());
|
||||
}
|
||||
|
||||
const List<TimeOfDay> _minuteMarkerValues = const <TimeOfDay>[
|
||||
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<TextPainter> _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<TimeOfDay> 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<TimeOfDay> _amHours = const <TimeOfDay>[
|
||||
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<TimeOfDay> _pmHours = const <TimeOfDay>[
|
||||
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<TextPainter> _build24HourInnerRing(TextTheme textTheme) {
|
||||
return _buildPainters(textTheme, _amHours
|
||||
.map((TimeOfDay timeOfDay) {
|
||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
||||
})
|
||||
.toList());
|
||||
}
|
||||
|
||||
List<TextPainter> _build24HourOuterRing(TextTheme textTheme) {
|
||||
return _buildPainters(textTheme, _pmHours
|
||||
.map((TimeOfDay timeOfDay) {
|
||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
||||
})
|
||||
.toList());
|
||||
}
|
||||
|
||||
List<TextPainter> _build12HourOuterRing(TextTheme textTheme) {
|
||||
return _buildPainters(textTheme, _amHours
|
||||
.map((TimeOfDay timeOfDay) {
|
||||
return localizations.formatHour(timeOfDay, alwaysUse24HourFormat: media.alwaysUse24HourFormat);
|
||||
})
|
||||
.toList());
|
||||
}
|
||||
|
||||
List<TextPainter> _buildMinutes(TextTheme textTheme) {
|
||||
const List<TimeOfDay> _minuteMarkerValues = const <TimeOfDay>[
|
||||
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<TextPainter> 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<TimeOfDay> showTimePicker({
|
||||
}) async {
|
||||
assert(context != null);
|
||||
assert(initialTime != null);
|
||||
return await showDialog(
|
||||
return await showDialog<TimeOfDay>(
|
||||
context: context,
|
||||
child: new _TimePickerDialog(initialTime: initialTime),
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged,
|
||||
{ Locale locale: const Locale('en', 'US') }) async {
|
||||
await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: locale,));
|
||||
Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> 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<Null> 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<String> labels12To11 = const <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
|
||||
const List<String> labels12To11TwoDigit = const <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
||||
const List<String> labels00To23 = const <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
||||
|
||||
Future<Null> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
||||
await tester.pumpWidget(
|
||||
new Localizations(
|
||||
locale: const Locale('en', 'US'),
|
||||
delegates: <LocalizationsDelegate<dynamic>>[
|
||||
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<dynamic>(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<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
||||
expect(dialPainter.primaryInnerLabels, null);
|
||||
|
||||
final List<TextPainter> 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<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
||||
final List<TextPainter> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
||||
expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
||||
|
||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
||||
final List<TextPainter> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
||||
expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
||||
});
|
||||
}
|
||||
|
29
packages/flutter/test/material/time_test.dart
Normal file
29
packages/flutter/test/material/time_test.dart
Normal file
@ -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<String> 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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDa
|
||||
'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
|
||||
};
|
||||
|
||||
/// Finds the [TimeOfDayFormat] to use instead of the `original` when the
|
||||
/// `original` uses 12-hour format and [MediaQueryData.alwaysUse24HourFormat]
|
||||
/// is true.
|
||||
TimeOfDayFormat _get24HourVersionOf(TimeOfDayFormat original) {
|
||||
switch (original) {
|
||||
case TimeOfDayFormat.HH_colon_mm:
|
||||
case TimeOfDayFormat.HH_dot_mm:
|
||||
case TimeOfDayFormat.frenchCanadian:
|
||||
case TimeOfDayFormat.H_colon_mm:
|
||||
return original;
|
||||
case TimeOfDayFormat.h_colon_mm_space_a:
|
||||
case TimeOfDayFormat.a_space_h_colon_mm:
|
||||
return TimeOfDayFormat.HH_colon_mm;
|
||||
}
|
||||
return TimeOfDayFormat.HH_colon_mm;
|
||||
}
|
||||
|
||||
/// Tracks if date i18n data has been loaded.
|
||||
bool _dateIntlDataInitialized = false;
|
||||
|
||||
|
@ -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/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -24,40 +26,44 @@ void main() {
|
||||
});
|
||||
|
||||
group('formatHour', () {
|
||||
test('formats h', () {
|
||||
GlobalMaterialLocalizations localizations;
|
||||
Future<String> formatHour(WidgetTester tester, Locale locale, TimeOfDay timeOfDay) async {
|
||||
final Completer<String> completer = new Completer<String>();
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
supportedLocales: <Locale>[locale],
|
||||
locale: locale,
|
||||
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
||||
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<String> formatTimeOfDay(WidgetTester tester, Locale locale, TimeOfDay timeOfDay) async {
|
||||
final Completer<String> completer = new Completer<String>();
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
supportedLocales: <Locale>[locale],
|
||||
locale: locale,
|
||||
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -48,8 +48,7 @@ Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChange
|
||||
}
|
||||
|
||||
Future<Null> 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<Locale, List<String>> locales = <Locale, List<String>>{
|
||||
const Locale('en', 'US'): const <String>['hour h', 'string :', 'minute', 'period'], //'h:mm a'
|
||||
const Locale('en', 'GB'): const <String>['hour HH', 'string :', 'minute'], //'HH:mm'
|
||||
const Locale('es', 'ES'): const <String>['hour H', 'string :', 'minute'], //'H:mm'
|
||||
const Locale('fr', 'CA'): const <String>['hour HH', 'string h', 'minute'], //'HH \'h\' mm'
|
||||
const Locale('zh', 'ZH'): const <String>['period', 'hour h', 'string :', 'minute'], //'ah:mm'
|
||||
const Locale('en', 'US'): const <String>['hour', 'string :', 'minute', 'period'], //'h:mm a'
|
||||
const Locale('en', 'GB'): const <String>['hour', 'string :', 'minute'], //'HH:mm'
|
||||
const Locale('es', 'ES'): const <String>['hour', 'string :', 'minute'], //'H:mm'
|
||||
const Locale('fr', 'CA'): const <String>['hour', 'string h', 'minute'], //'HH \'h\' mm'
|
||||
const Locale('zh', 'ZH'): const <String>['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<String> labels12To11 = const <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
|
||||
const List<String> labels12To11TwoDigit = const <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
||||
const List<String> labels00To23 = const <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
||||
|
||||
Future<Null> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
||||
await tester.pumpWidget(
|
||||
new Localizations(
|
||||
locale: const Locale('en', 'US'),
|
||||
delegates: <LocalizationsDelegate<dynamic>>[
|
||||
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<dynamic>(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<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels12To11);
|
||||
expect(dialPainter.primaryInnerLabels, null);
|
||||
|
||||
final List<TextPainter> 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<TextPainter> primaryOuterLabels = dialPainter.primaryOuterLabels;
|
||||
expect(primaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
||||
final List<TextPainter> primaryInnerLabels = dialPainter.primaryInnerLabels;
|
||||
expect(primaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
||||
|
||||
final List<TextPainter> secondaryOuterLabels = dialPainter.secondaryOuterLabels;
|
||||
expect(secondaryOuterLabels.map((TextPainter tp) => tp.text.text), labels00To23);
|
||||
final List<TextPainter> secondaryInnerLabels = dialPainter.secondaryInnerLabels;
|
||||
expect(secondaryInnerLabels.map((TextPainter tp) => tp.text.text), labels12To11TwoDigit);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user