Commit cbdefead authored by Yegor's avatar Yegor Committed by GitHub

Localize time picker (#11967)

* Internationalize the time picker

- header layout and formatting
- 12-hour vs 24-hour dial
- RTL

* make TimeOfDayFormat an enum

* address comments
parent 72cc92cb
...@@ -76,7 +76,7 @@ String generateLocalizationsMap() { ...@@ -76,7 +76,7 @@ String generateLocalizationsMap() {
const Map<String, Map<String, String>> localizations = const <String, Map<String, String>> {'''); const Map<String, Map<String, String>> localizations = const <String, Map<String, String>> {''');
final String lastLocale = localeToResources.keys.last; final String lastLocale = localeToResources.keys.last;
for (String locale in localeToResources.keys) { for (String locale in localeToResources.keys.toList()..sort()) {
output.writeln(' "$locale": const <String, String>{'); output.writeln(' "$locale": const <String, String>{');
final Map<String, String> resources = localeToResources[locale]; final Map<String, String> resources = localeToResources[locale];
...@@ -126,7 +126,7 @@ void main(List<String> args) { ...@@ -126,7 +126,7 @@ void main(List<String> args) {
} }
} }
final String regenerate = 'dart gen_localizations ${directory.path} ${args[1]}'; final String regenerate = 'dart dev/tools/gen_localizations.dart ${directory.path} ${args[1]}';
print(outputHeader.replaceFirst('@(regenerate)', regenerate)); print(outputHeader.replaceFirst('@(regenerate)', regenerate));
print(generateLocalizationsMap()); print(generateLocalizationsMap());
} }
{ {
"timeOfDayFormat": "h:mm a",
"openAppDrawerTooltip": "افتح قائمة التنقل", "openAppDrawerTooltip": "افتح قائمة التنقل",
"backButtonTooltip": "الى الخلف", "backButtonTooltip": "الى الخلف",
"closeButtonTooltip": "إغلا", "closeButtonTooltip": "إغلا",
...@@ -20,5 +21,7 @@ ...@@ -20,5 +21,7 @@
"okButtonLabel": "حسنا", "okButtonLabel": "حسنا",
"pasteButtonLabel": "عجين", "pasteButtonLabel": "عجين",
"selectAllButtonLabel": "اختر الكل", "selectAllButtonLabel": "اختر الكل",
"viewLicensesButtonLabel": "عرض التراخيص" "viewLicensesButtonLabel": "عرض التراخيص",
"anteMeridiemAbbreviation": "ص",
"postMeridiemAbbreviation": "م"
} }
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "Navigationsmenü öffnen", "openAppDrawerTooltip": "Navigationsmenü öffnen",
"backButtonTooltip": "Zurück", "backButtonTooltip": "Zurück",
"closeButtonTooltip": "Schließen", "closeButtonTooltip": "Schließen",
......
{ {
"timeOfDayFormat": "h:mm a",
"@timeOfDayFormat": {
"description": "The ICU 'Short Time' pattern, such as 'HH:mm', 'h:mm a', 'H:mm'. See: http://demo.icu-project.org/icu-bin/locexp?d_=en&_=en_US",
"type": "text"
},
"openAppDrawerTooltip": "Open navigation menu", "openAppDrawerTooltip": "Open navigation menu",
"@openAppDrawerTooltip": { "@openAppDrawerTooltip": {
"description": "The tooltip for the leading AppBar menu (aka 'hamburger') button", "description": "The tooltip for the leading AppBar menu (aka 'hamburger') button",
...@@ -126,5 +132,17 @@ ...@@ -126,5 +132,17 @@
"@viewLicensesButtonLabel": { "@viewLicensesButtonLabel": {
"description": "The label for the about box's view licenses button.", "description": "The label for the about box's view licenses button.",
"type": "text" "type": "text"
},
"anteMeridiemAbbreviation": "AM",
"@anteMeridiemAbbreviation": {
"description": "The abbreviation for ante meridiem (before noon) shown in the time picker.",
"type": "text"
},
"postMeridiemAbbreviation": "PM",
"@postMeridiemAbbreviation": {
"description": "The abbreviation for post meridiem (after noon) shown in the time picker.",
"type": "text"
} }
} }
{ {
"timeOfDayFormat": "H:mm",
"openAppDrawerTooltip": "Abrir el menú de navegación", "openAppDrawerTooltip": "Abrir el menú de navegación",
"backButtonTooltip": "Espalda", "backButtonTooltip": "Espalda",
"closeButtonTooltip": "Cerrar", "closeButtonTooltip": "Cerrar",
......
{ {
"timeOfDayFormat": "H:mm",
"openAppDrawerTooltip": "منوی ناوبری را باز کنید", "openAppDrawerTooltip": "منوی ناوبری را باز کنید",
"backButtonTooltip": "بازگشت", "backButtonTooltip": "بازگشت",
"closeButtonTooltip": "بستن", "closeButtonTooltip": "بستن",
......
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "Ouvrir le menu de navigation", "openAppDrawerTooltip": "Ouvrir le menu de navigation",
"backButtonTooltip": "Retour", "backButtonTooltip": "Retour",
"closeButtonTooltip": "Fermer", "closeButtonTooltip": "Fermer",
......
{ {
"timeOfDayFormat": "H:mm",
"openAppDrawerTooltip": "פתח תפריט ניווט", "openAppDrawerTooltip": "פתח תפריט ניווט",
"backButtonTooltip": "אחורה", "backButtonTooltip": "אחורה",
"closeButtonTooltip": "סגור", "closeButtonTooltip": "סגור",
......
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "Apri il menu di navigazione", "openAppDrawerTooltip": "Apri il menu di navigazione",
"backButtonTooltip": "Indietro", "backButtonTooltip": "Indietro",
"closeButtonTooltip": "Chiudi", "closeButtonTooltip": "Chiudi",
......
{ {
"timeOfDayFormat": "H:mm",
"openAppDrawerTooltip": "ナビゲーションメニューを開く", "openAppDrawerTooltip": "ナビゲーションメニューを開く",
"backButtonTooltip": "戻る", "backButtonTooltip": "戻る",
"closeButtonTooltip": "閉じる", "closeButtonTooltip": "閉じる",
......
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "د پرانیستی نیینګ مینو", "openAppDrawerTooltip": "د پرانیستی نیینګ مینو",
"backButtonTooltip": "شاته", "backButtonTooltip": "شاته",
"closeButtonTooltip": "بنده", "closeButtonTooltip": "بنده",
......
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "Abrir menu de navegação", "openAppDrawerTooltip": "Abrir menu de navegação",
"backButtonTooltip": "Costas", "backButtonTooltip": "Costas",
"closeButtonTooltip": "Fechar", "closeButtonTooltip": "Fechar",
......
{ {
"timeOfDayFormat": "H:mm",
"openAppDrawerTooltip": "Открыть меню навигации", "openAppDrawerTooltip": "Открыть меню навигации",
"backButtonTooltip": "назад", "backButtonTooltip": "назад",
"closeButtonTooltip": "Закрыть", "closeButtonTooltip": "Закрыть",
......
{ {
"timeOfDayFormat": "HH:mm",
"openAppDrawerTooltip": "اوپن جي مينڊيٽ مينيو", "openAppDrawerTooltip": "اوپن جي مينڊيٽ مينيو",
"backButtonTooltip": "پوئتي", "backButtonTooltip": "پوئتي",
"closeButtonTooltip": "بند ڪريو", "closeButtonTooltip": "بند ڪريو",
......
{ {
"timeOfDayFormat": "h:mm a",
"openAppDrawerTooltip": "کھولیں نیویگیشن مینو", "openAppDrawerTooltip": "کھولیں نیویگیشن مینو",
"backButtonTooltip": "واپس", "backButtonTooltip": "واپس",
"closeButtonTooltip": "بند کریں", "closeButtonTooltip": "بند کریں",
...@@ -20,5 +21,7 @@ ...@@ -20,5 +21,7 @@
"okButtonLabel": "ٹھیک ہے", "okButtonLabel": "ٹھیک ہے",
"pasteButtonLabel": "چسپاں", "pasteButtonLabel": "چسپاں",
"selectAllButtonLabel": "تکاپیمام منتخب کریں", "selectAllButtonLabel": "تکاپیمام منتخب کریں",
"viewLicensesButtonLabel": "لائسنس دیکھیں" "viewLicensesButtonLabel": "لائسنس دیکھیں",
"anteMeridiemAbbreviation": "AM",
"postMeridiemAbbreviation": "PM"
} }
\ No newline at end of file
{ {
"timeOfDayFormat": "ah:mm",
"openAppDrawerTooltip": "打开导航菜单", "openAppDrawerTooltip": "打开导航菜单",
"backButtonTooltip": "返回", "backButtonTooltip": "返回",
"closeButtonTooltip": "关闭", "closeButtonTooltip": "关闭",
...@@ -20,5 +21,11 @@ ...@@ -20,5 +21,11 @@
"okButtonLabel": "确定", "okButtonLabel": "确定",
"pasteButtonLabel": "粘贴", "pasteButtonLabel": "粘贴",
"selectAllButtonLabel": "全选", "selectAllButtonLabel": "全选",
"viewLicensesButtonLabel": "查看许可证" "viewLicensesButtonLabel": "查看许可证",
"backButtonTooltip": "背部",
"closeButtonTooltip": "关",
"nextMonthTooltip": "-下月就29了。",
"previousMonthTooltip": "前一个月",
"anteMeridiemAbbreviation": "上午",
"postMeridiemAbbreviation": "下午"
} }
...@@ -10,7 +10,7 @@ import 'package:intl/intl.dart' as intl; ...@@ -10,7 +10,7 @@ import 'package:intl/intl.dart' as intl;
import 'i18n/localizations.dart'; import 'i18n/localizations.dart';
/// Defines the localized resource values used by the Material widgts. /// Defines the localized resource values used by the Material widgets.
/// ///
/// See also: /// See also:
/// ///
...@@ -80,6 +80,18 @@ abstract class MaterialLocalizations { ...@@ -80,6 +80,18 @@ abstract class MaterialLocalizations {
/// Label for the [AboutBox] button that shows the [LicensePage]. /// Label for the [AboutBox] button that shows the [LicensePage].
String get viewLicensesButtonLabel; String get viewLicensesButtonLabel;
/// The abbreviation for ante meridiem (before noon) shown in the time picker.
String get anteMeridiemAbbreviation;
/// The abbreviation for post meridiem (after noon) shown in the time picker.
String get postMeridiemAbbreviation;
/// The format used to lay out the time picker.
///
/// The documentation for [TimeOfDayFormat] enum values provides details on
/// each supported layout.
TimeOfDayFormat get timeOfDayFormat;
/// The `MaterialLocalizations` from the closest [Localizations] instance /// The `MaterialLocalizations` from the closest [Localizations] instance
/// that encloses the given context. /// that encloses the given context.
/// ///
...@@ -99,25 +111,30 @@ abstract class MaterialLocalizations { ...@@ -99,25 +111,30 @@ abstract class MaterialLocalizations {
/// Localized strings for the material widgets. /// Localized strings for the material widgets.
class DefaultMaterialLocalizations implements MaterialLocalizations { class DefaultMaterialLocalizations implements MaterialLocalizations {
/// Construct an object that defines the material widgets' localized strings /// Constructs an object that defines the material widgets' localized strings
/// for the given `locale`. /// for the given `locale`.
/// ///
/// [LocalizationsDelegate] implementations typically call the static [load] /// [LocalizationsDelegate] implementations typically call the static [load]
/// function, rather than constructing this class directly. /// function, rather than constructing this class directly.
DefaultMaterialLocalizations(this.locale) { factory DefaultMaterialLocalizations(Locale locale) {
assert(locale != null); assert(locale != null);
_nameToValue = localizations[_localeName]
?? localizations[locale.languageCode] final Map<String, String> result = <String, String>{};
?? localizations['en'] if (localizations.containsKey(locale.languageCode))
?? <String, String>{}; result.addAll(localizations[locale.languageCode]);
if (localizations.containsKey(locale.toString()))
result.addAll(localizations[locale.toString()]);
return new DefaultMaterialLocalizations._(locale, result);
} }
Map<String, String> _nameToValue; DefaultMaterialLocalizations._(this.locale, this._nameToValue);
/// The locale for which the values of this class's localized resources /// The locale for which the values of this class's localized resources
/// have been translated. /// have been translated.
final Locale locale; final Locale locale;
final Map<String, String> _nameToValue;
String get _localeName { String get _localeName {
final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString(); final String localeName = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
return intl.Intl.canonicalizedLocale(localeName); return intl.Intl.canonicalizedLocale(localeName);
...@@ -224,6 +241,47 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -224,6 +241,47 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
@override @override
String get viewLicensesButtonLabel => _nameToValue['viewLicensesButtonLabel']; String get viewLicensesButtonLabel => _nameToValue['viewLicensesButtonLabel'];
@override
String get anteMeridiemAbbreviation => _nameToValue['anteMeridiemAbbreviation'];
@override
String get postMeridiemAbbreviation => _nameToValue['postMeridiemAbbreviation'];
/// The [TimeOfDayFormat] corresponding to one of the following supported
/// patterns:
///
/// * `HH:mm`
/// * `HH.mm`
/// * `HH 'h' mm`
/// * `HH:mm น.`
/// * `H:mm`
/// * `h:mm a`
/// * `a h:mm`
/// * `ah:mm`
///
/// See also:
///
/// * 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 {
final String icuShortTimePattern = _nameToValue['timeOfDayFormat'];
assert(() {
if (!_icuTimeOfDayToEnum.containsKey(icuShortTimePattern)) {
throw new FlutterError(
'"$icuShortTimePattern" is not one of the ICU short time patterns '
'supported by the material library. Here is the list of supported '
'patterns:\n ' +
_icuTimeOfDayToEnum.keys.join('\n ')
);
}
return true;
});
return _icuTimeOfDayToEnum[icuShortTimePattern];
}
/// Creates an object that provides localized resource values for the /// Creates an object that provides localized resource values for the
/// for the widgets of the material library. /// for the widgets of the material library.
/// ///
...@@ -233,3 +291,66 @@ class DefaultMaterialLocalizations implements MaterialLocalizations { ...@@ -233,3 +291,66 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
return new SynchronousFuture<MaterialLocalizations>(new DefaultMaterialLocalizations(locale)); return new SynchronousFuture<MaterialLocalizations>(new DefaultMaterialLocalizations(locale));
} }
} }
const Map<String, TimeOfDayFormat> _icuTimeOfDayToEnum = const <String, TimeOfDayFormat>{
'HH:mm': TimeOfDayFormat.HH_colon_mm,
'HH.mm': TimeOfDayFormat.HH_dot_mm,
"HH 'h' mm": TimeOfDayFormat.frenchCanadian,
'HH:mm น.': TimeOfDayFormat.HH_colon_mm,
'H:mm': TimeOfDayFormat.H_colon_mm,
'h:mm a': TimeOfDayFormat.h_colon_mm_space_a,
'a h:mm': TimeOfDayFormat.a_space_h_colon_mm,
'ah:mm': TimeOfDayFormat.a_space_h_colon_mm,
};
/// Determines how the time picker invoked using [showTimePicker] formats and
/// lays out the time controls.
///
/// The time picker provides layout configurations optimized for each of the
/// enum values.
enum TimeOfDayFormat {
/// Corresponds to the ICU 'HH:mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one colon
/// character.
HH_colon_mm,
/// Corresponds to the ICU 'HH.mm' pattern.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by one dot
/// character.
HH_dot_mm,
/// Corresponds to the ICU "HH 'h' mm" pattern used in Canadian French.
///
/// This format uses 24-hour two-digit zero-padded hours. Controls are always
/// laid out horizontally. Hours are separated from minutes by letter 'h'.
frenchCanadian,
/// Corresponds to the ICU 'H:mm' pattern.
///
/// This format uses 24-hour non-padded variable-length hours. Controls are
/// always laid out horizontally. Hours are separated from minutes by one
/// colon character.
H_colon_mm,
/// Corresponds to the ICU 'h:mm a' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically after (consistent with the ambient
/// [TextDirection]) hour-minute indicator. Hours are separated from minutes
/// by one colon character.
h_colon_mm_space_a,
/// Corresponds to the ICU 'a h:mm' pattern.
///
/// This format uses 12-hour non-padded variable-length hours with a day
/// period. Controls are laid out horizontally in portrait mode. In landscape
/// mode, the day period appears vertically before (consistent with the
/// ambient [TextDirection]) hour-minute indicator. Hours are separated from
/// minutes by one colon character.
a_space_h_colon_mm,
}
...@@ -43,7 +43,7 @@ AxisDirection _getDefaultCrossAxisDirection(BuildContext context, AxisDirection ...@@ -43,7 +43,7 @@ AxisDirection _getDefaultCrossAxisDirection(BuildContext context, AxisDirection
/// example, if the [axisDirection] is [AxisDirection.down], the first sliver /// example, if the [axisDirection] is [AxisDirection.down], the first sliver
/// before [center] is placed above the [center]. The slivers that are later in /// before [center] is placed above the [center]. The slivers that are later in
/// the child list than [center] are placed in order in the [axisDirection]. For /// the child list than [center] are placed in order in the [axisDirection]. For
/// example, in the preceeding scenario, the first sliver after [center] is /// example, in the preceding scenario, the first sliver after [center] is
/// placed below the [center]. /// placed below the [center].
/// ///
/// [Viewport] cannot contain box children directly. Instead, use a /// [Viewport] cannot contain box children directly. Instead, use a
......
...@@ -2,19 +2,24 @@ ...@@ -2,19 +2,24 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
class _TimePickerLauncher extends StatelessWidget { class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({ Key key, this.onChanged }) : super(key: key); const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key);
final ValueChanged<TimeOfDay> onChanged; final ValueChanged<TimeOfDay> onChanged;
final Locale locale;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new MaterialApp( return new MaterialApp(
locale: locale,
home: new Material( home: new Material(
child: new Center( child: new Center(
child: new Builder( child: new Builder(
...@@ -36,15 +41,18 @@ class _TimePickerLauncher extends StatelessWidget { ...@@ -36,15 +41,18 @@ class _TimePickerLauncher extends StatelessWidget {
} }
} }
Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged) async { Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged,
await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged)); { Locale locale: const Locale('en', 'US') }) async {
await tester.pumpWidget(new _TimePickerLauncher(onChanged: onChanged, locale: locale,));
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
return tester.getCenter(find.byKey(const Key('time-picker-dial'))); return tester.getCenter(find.byKey(const Key('time-picker-dial')));
} }
Future<Null> finishPicker(WidgetTester tester) async { Future<Null> finishPicker(WidgetTester tester) async {
await tester.tap(find.text('OK')); final Element timePickerElement = tester.element(find.byElementPredicate((Element element) => element.widget.runtimeType.toString() == '_TimePickerDialog'));
final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(timePickerElement);
await tester.tap(find.text(materialLocalizations.okButtonLabel));
await tester.pumpAndSettle(const Duration(seconds: 1)); await tester.pumpAndSettle(const Duration(seconds: 1));
} }
...@@ -197,4 +205,78 @@ void main() { ...@@ -197,4 +205,78 @@ void main() {
expect(feedback.hapticCount, 3); expect(feedback.hapticCount, 3);
}); });
}); });
group('localization', () {
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'
};
for (Locale locale in locales.keys) {
final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale);
final List<String> actual = <String>[];
tester.element(find.byType(CustomMultiChildLayout)).visitChildren((Element child) {
final LayoutId layout = child.widget;
final String fragmentType = '${layout.child.runtimeType}';
final dynamic widget = layout.child;
if (fragmentType == '_MinuteControl') {
actual.add('minute');
} else if (fragmentType == '_DayPeriodControl') {
actual.add('period');
} else if (fragmentType == '_HourControl') {
actual.add('hour ${widget.hourFormat.toString().split('.').last}');
} else if (fragmentType == '_StringFragment') {
actual.add('string ${widget.value}');
} else {
fail('Unsupported fragment type: $fragmentType');
}
});
expect(actual, locales[locale]);
await tester.tapAt(new Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
}
});
testWidgets('uses single-ring 12-hour dial for h hour format', (WidgetTester tester) async {
// Tap along the segment stretching from the center to the edge at
// 12:00 AM position. Because there's only one ring, no matter where you
// tap the time will be the same. See the 24-hour dial test that behaves
// differently.
for (int i = 1; i < 10; i++) {
TimeOfDay result;
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
final Size size = tester.getSize(find.byKey(const Key('time-picker-dial')));
final double dy = (size.height / 2.0 / 10) * i;
await tester.tapAt(new Offset(center.dx, center.dy - dy));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
}
});
testWidgets('uses two-ring 24-hour dial for H and HH hour formats', (WidgetTester tester) async {
const List<Locale> locales = const <Locale>[
const Locale('en', 'GB'), // HH
const Locale('es', 'ES'), // H
];
for (Locale locale in locales) {
// Tap along the segment stretching from the center to the edge at
// 12:00 AM position. There are two rings. At ~70% mark, the ring
// switches between inner ring and outer ring.
for (int i = 1; i < 10; i++) {
TimeOfDay result;
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale);
final Size size = tester.getSize(find.byKey(const Key('time-picker-dial')));
final double dy = (size.height / 2.0 / 10) * i;
await tester.tapAt(new Offset(center.dx, center.dy - dy));
await finishPicker(tester);
expect(result, equals(new TimeOfDay(hour: i < 7 ? 12 : 0, minute: 0)));
}
}
});
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment