[Material] Redesign Time Picker (#59191)

This commit is contained in:
rami-a 2020-06-22 20:03:04 -04:00 committed by GitHub
parent 2cd205bb5a
commit e676024d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2416 additions and 1291 deletions

View File

@ -124,6 +124,7 @@ export 'src/material/theme.dart';
export 'src/material/theme_data.dart';
export 'src/material/time.dart';
export 'src/material/time_picker.dart';
export 'src/material/time_picker_theme.dart';
export 'src/material/toggle_buttons.dart';
export 'src/material/toggle_buttons_theme.dart';
export 'src/material/toggleable.dart';

View File

@ -35,6 +35,7 @@ import 'slider_theme.dart';
import 'snack_bar_theme.dart';
import 'tab_bar_theme.dart';
import 'text_theme.dart';
import 'time_picker_theme.dart';
import 'toggle_buttons_theme.dart';
import 'tooltip_theme.dart';
import 'typography.dart';
@ -269,6 +270,7 @@ class ThemeData with Diagnosticable {
DividerThemeData dividerTheme,
ButtonBarThemeData buttonBarTheme,
BottomNavigationBarThemeData bottomNavigationBarTheme,
TimePickerThemeData timePickerTheme,
bool fixTextFieldOutlineLabel,
}) {
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
@ -380,6 +382,7 @@ class ThemeData with Diagnosticable {
dividerTheme ??= const DividerThemeData();
buttonBarTheme ??= const ButtonBarThemeData();
bottomNavigationBarTheme ??= const BottomNavigationBarThemeData();
timePickerTheme ??= const TimePickerThemeData();
fixTextFieldOutlineLabel ??= false;
@ -448,6 +451,7 @@ class ThemeData with Diagnosticable {
dividerTheme: dividerTheme,
buttonBarTheme: buttonBarTheme,
bottomNavigationBarTheme: bottomNavigationBarTheme,
timePickerTheme: timePickerTheme,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
);
}
@ -527,6 +531,7 @@ class ThemeData with Diagnosticable {
@required this.dividerTheme,
@required this.buttonBarTheme,
@required this.bottomNavigationBarTheme,
@required this.timePickerTheme,
@required this.fixTextFieldOutlineLabel,
}) : assert(visualDensity != null),
assert(primaryColor != null),
@ -589,6 +594,7 @@ class ThemeData with Diagnosticable {
assert(dividerTheme != null),
assert(buttonBarTheme != null),
assert(bottomNavigationBarTheme != null),
assert(timePickerTheme != null),
assert(fixTextFieldOutlineLabel != null);
/// Create a [ThemeData] based on the colors in the given [colorScheme] and
@ -1036,6 +1042,9 @@ class ThemeData with Diagnosticable {
/// widgets.
final BottomNavigationBarThemeData bottomNavigationBarTheme;
/// A theme for customizing the appearance and layout of time picker widgets.
final TimePickerThemeData timePickerTheme;
/// A temporary flag to allow apps to opt-in to a
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
@ -1117,6 +1126,7 @@ class ThemeData with Diagnosticable {
DividerThemeData dividerTheme,
ButtonBarThemeData buttonBarTheme,
BottomNavigationBarThemeData bottomNavigationBarTheme,
TimePickerThemeData timePickerTheme,
bool fixTextFieldOutlineLabel,
}) {
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
@ -1185,6 +1195,7 @@ class ThemeData with Diagnosticable {
dividerTheme: dividerTheme ?? this.dividerTheme,
buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme,
bottomNavigationBarTheme: bottomNavigationBarTheme ?? this.bottomNavigationBarTheme,
timePickerTheme: timePickerTheme ?? this.timePickerTheme,
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel,
);
}
@ -1331,6 +1342,7 @@ class ThemeData with Diagnosticable {
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t),
bottomNavigationBarTheme: BottomNavigationBarThemeData.lerp(a.bottomNavigationBarTheme, b.bottomNavigationBarTheme, t),
timePickerTheme: TimePickerThemeData.lerp(a.timePickerTheme, b.timePickerTheme, t),
fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
);
}
@ -1405,6 +1417,7 @@ class ThemeData with Diagnosticable {
&& other.dividerTheme == dividerTheme
&& other.buttonBarTheme == buttonBarTheme
&& other.bottomNavigationBarTheme == bottomNavigationBarTheme
&& other.timePickerTheme == timePickerTheme
&& other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel;
}
@ -1478,6 +1491,7 @@ class ThemeData with Diagnosticable {
dividerTheme,
buttonBarTheme,
bottomNavigationBarTheme,
timePickerTheme,
fixTextFieldOutlineLabel,
];
return hashList(values);
@ -1547,6 +1561,7 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ButtonBarThemeData>('buttonBarTheme', buttonBarTheme, defaultValue: defaultData.buttonBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<TimePickerThemeData>('timePickerTheme', timePickerTheme, defaultValue: defaultData.timePickerTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<BottomNavigationBarThemeData>('bottomNavigationBarTheme', bottomNavigationBarTheme, defaultValue: defaultData.bottomNavigationBarTheme, level: DiagnosticLevel.debug));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,381 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'input_decorator.dart';
import 'theme.dart';
/// Defines the visual properties of the widget displayed with [showTimePicker].
///
/// Descendant widgets obtain the current [TimePickerThemeData] object using
/// `TimePickerTheme.of(context)`. Instances of [TimePickerThemeData]
/// can be customized with [TimePickerThemeData.copyWith].
///
/// Typically a [TimePickerThemeData] is specified as part of the overall
/// [Theme] with [ThemeData.timePickerTheme].
///
/// All [TimePickerThemeData] properties are `null` by default. When null,
/// [showTimePicker] will provide its own defaults.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme information for the
/// application.
/// * [TimePickerTheme], which describes the actual configuration of a time
/// picker theme.
@immutable
class TimePickerThemeData with Diagnosticable {
/// Creates a theme that can be used for [TimePickerTheme] or
/// [ThemeData.timePickerTheme].
const TimePickerThemeData({
this.backgroundColor,
this.hourMinuteTextColor,
this.hourMinuteColor,
this.dayPeriodTextColor,
this.dayPeriodColor,
this.dialHandColor,
this.dialBackgroundColor,
this.entryModeIconColor,
this.hourMinuteTextStyle,
this.dayPeriodTextStyle,
this.helpTextStyle,
this.shape,
this.hourMinuteShape,
this.dayPeriodShape,
this.dayPeriodBorderSide,
this.inputDecorationTheme,
});
/// The background color of a time picker.
///
/// If this is null, the time picker defaults to the overall theme's
/// [ColorScheme.background].
final Color backgroundColor;
/// The color of the header text that represents hours and minutes.
///
/// If [hourMinuteTextColor] is a [MaterialStateColor], then the effective
/// text color can depend on the [MaterialState.selected] state, i.e. if the
/// text is selected or not.
///
/// By default the overall theme's [ColorScheme.primary] color is used when
/// the text is selected and [ColorScheme.onSurface] when it's not selected.
final Color hourMinuteTextColor;
/// The background color of the hour and minutes header segments.
///
/// If [hourMinuteColor] is a [MaterialStateColor], then the effective
/// background color can depend on the [MaterialState.selected] state, i.e.
/// if the segment is selected or not.
///
/// By default, if the segment is selected, the overall theme's
/// `ColorScheme.primary.withOpacity(0.12)` is used when the overall theme's
/// brightness is [Brightness.light] and
/// `ColorScheme.primary.withOpacity(0.24)` is used when the overall theme's
/// brightness is [Brightness.dark].
/// If the segment is not selected, the overall theme's
/// `ColorScheme.onSurface.withOpacity(0.12)` is used.
final Color hourMinuteColor;
/// The color of the day period text that represents AM/PM.
///
/// If [dayPeriodTextColor] is a [MaterialStateColor], then the effective
/// text color can depend on the [MaterialState.selected] state, i.e. if the
/// text is selected or not.
///
/// By default the overall theme's [ColorScheme.primary] color is used when
/// the text is selected and `ColorScheme.onSurface.withOpacity(0.60)` when
/// it's not selected.
final Color dayPeriodTextColor;
/// The background color of the AM/PM toggle.
///
/// If [dayPeriodColor] is a [MaterialStateColor], then the effective
/// background color can depend on the [MaterialState.selected] state, i.e.
/// if the segment is selected or not.
///
/// By default, if the segment is selected, the overall theme's
/// `ColorScheme.primary.withOpacity(0.12)` is used when the overall theme's
/// brightness is [Brightness.light] and
/// `ColorScheme.primary.withOpacity(0.24)` is used when the overall theme's
/// brightness is [Brightness.dark].
/// If the segment is not selected, [Colors.transparent] is used to allow the
/// [Dialog]'s color to be used.
final Color dayPeriodColor;
/// The color of the time picker dial's hand.
///
/// If this is null, the time picker defaults to the overall theme's
/// [ColorScheme.primary].
final Color dialHandColor;
/// The background color of the time picker dial.
///
/// If this is null, the time picker defaults to the overall theme's
/// [ColorScheme.primary].
final Color dialBackgroundColor;
/// The color of the entry mode [IconButton].
///
/// If this is null, the time picker defaults to
/// ```
/// Theme.of(context).colorScheme.onSurface.withOpacity(
/// Theme.of(context).colorScheme.brightness == Brightness.dark ? 1.0 : 0.6,
/// )
/// ```
final Color entryModeIconColor;
/// Used to configure the [TextStyle]s for the hour/minute controls.
///
/// If this is null, the time picker defaults to the overall theme's
/// [TextTheme.headline3].
final TextStyle hourMinuteTextStyle;
/// Used to configure the [TextStyle]s for the day period control.
///
/// If this is null, the time picker defaults to the overall theme's
/// [TextTheme.subtitle1].
final TextStyle dayPeriodTextStyle;
/// Used to configure the [TextStyle]s for the helper text in the header.
///
/// If this is null, the time picker defaults to the overall theme's
/// [TextTheme.overline].
final TextStyle helpTextStyle;
/// The shape of the [Dialog] that the time picker is presented in.
///
/// If this is null, the time picker defaults to
/// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
final ShapeBorder shape;
/// The shape of the hour and minute controls that the time picker uses.
///
/// If this is null, the time picker defaults to
/// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
final ShapeBorder hourMinuteShape;
/// The shape of the day period that the time picker uses.
///
/// If this is null, the time picker defaults to:
/// ```
/// RoundedRectangleBorder(
/// borderRadius: BorderRadius.all(Radius.circular(4.0)),
/// side: BorderSide(),
/// )
/// ```
final OutlinedBorder dayPeriodShape;
/// The color and weight of the day period's outline.
///
/// If this is null, the time picker defaults to:
/// ```
/// BorderSide(
/// color: Color.alphaBlend(colorScheme.onBackground.withOpacity(0.38), colorScheme.surface),
/// )
/// ```
final BorderSide dayPeriodBorderSide;
/// The input decoration theme for the [TextField]s in the time picker.
///
/// If this is null, the time picker provides its own defaults.
final InputDecorationTheme inputDecorationTheme;
/// Creates a copy of this object with the given fields replaced with the
/// new values.
TimePickerThemeData copyWith({
Color backgroundColor,
Color hourMinuteTextColor,
Color hourMinuteColor,
Color hourMinuteUnselectedTextColor,
Color hourMinuteUnselectedColor,
Color dayPeriodTextColor,
Color dayPeriodColor,
Color dialHandColor,
Color dialBackgroundColor,
Color entryModeIconColor,
TextStyle hourMinuteTextStyle,
TextStyle dayPeriodTextStyle,
TextStyle helpTextStyle,
ShapeBorder shape,
ShapeBorder hourMinuteShape,
OutlinedBorder dayPeriodShape,
BorderSide dayPeriodBorderSide,
InputDecorationTheme inputDecorationTheme,
}) {
return TimePickerThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
hourMinuteTextColor: hourMinuteTextColor ?? this.hourMinuteTextColor,
hourMinuteColor: hourMinuteColor ?? this.hourMinuteColor,
dayPeriodTextColor: dayPeriodTextColor ?? this.dayPeriodTextColor,
dayPeriodColor: dayPeriodColor ?? this.dayPeriodColor,
dialHandColor: dialHandColor ?? this.dialHandColor,
dialBackgroundColor: dialBackgroundColor ?? this.dialBackgroundColor,
entryModeIconColor: entryModeIconColor ?? this.entryModeIconColor,
hourMinuteTextStyle: hourMinuteTextStyle ?? this.hourMinuteTextStyle,
dayPeriodTextStyle: dayPeriodTextStyle ?? this.dayPeriodTextStyle,
helpTextStyle: helpTextStyle ?? this.helpTextStyle,
shape: shape ?? this.shape,
hourMinuteShape: hourMinuteShape ?? this.hourMinuteShape,
dayPeriodShape: dayPeriodShape ?? this.dayPeriodShape,
dayPeriodBorderSide: dayPeriodBorderSide ?? this.dayPeriodBorderSide,
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
);
}
/// Linearly interpolate between two time picker themes.
///
/// The argument `t` must not be null.
///
/// {@macro dart.ui.shadow.lerp}
static TimePickerThemeData lerp(TimePickerThemeData a, TimePickerThemeData b, double t) {
assert(t != null);
// Workaround since BorderSide's lerp does not allow for null arguments.
BorderSide lerpedBorderSide;
if (a?.dayPeriodBorderSide == null && b?.dayPeriodBorderSide == null) {
lerpedBorderSide = null;
} else if (a?.dayPeriodBorderSide == null) {
lerpedBorderSide = b?.dayPeriodBorderSide;
} else if (b?.dayPeriodBorderSide == null) {
lerpedBorderSide = a?.dayPeriodBorderSide;
} else {
lerpedBorderSide = BorderSide.lerp(a?.dayPeriodBorderSide, b?.dayPeriodBorderSide, t);
}
return TimePickerThemeData(
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
hourMinuteTextColor: Color.lerp(a?.hourMinuteTextColor, b?.hourMinuteTextColor, t),
hourMinuteColor: Color.lerp(a?.hourMinuteColor, b?.hourMinuteColor, t),
dayPeriodTextColor: Color.lerp(a?.dayPeriodTextColor, b?.dayPeriodTextColor, t),
dayPeriodColor: Color.lerp(a?.dayPeriodColor, b?.dayPeriodColor, t),
dialHandColor: Color.lerp(a?.dialHandColor, b?.dialHandColor, t),
dialBackgroundColor: Color.lerp(a?.dialBackgroundColor, b?.dialBackgroundColor, t),
entryModeIconColor: Color.lerp(a?.entryModeIconColor, b?.entryModeIconColor, t),
hourMinuteTextStyle: TextStyle.lerp(a?.hourMinuteTextStyle, b?.hourMinuteTextStyle, t),
dayPeriodTextStyle: TextStyle.lerp(a?.dayPeriodTextStyle, b?.dayPeriodTextStyle, t),
helpTextStyle: TextStyle.lerp(a?.helpTextStyle, b?.helpTextStyle, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
hourMinuteShape: ShapeBorder.lerp(a?.hourMinuteShape, b?.hourMinuteShape, t),
dayPeriodShape: ShapeBorder.lerp(a?.dayPeriodShape, b?.dayPeriodShape, t) as OutlinedBorder,
dayPeriodBorderSide: lerpedBorderSide,
inputDecorationTheme: t < 0.5 ? a.inputDecorationTheme : b.inputDecorationTheme,
);
}
@override
int get hashCode {
return hashValues(
backgroundColor,
hourMinuteTextColor,
hourMinuteColor,
dayPeriodTextColor,
dayPeriodColor,
dialHandColor,
dialBackgroundColor,
entryModeIconColor,
hourMinuteTextStyle,
dayPeriodTextStyle,
helpTextStyle,
shape,
hourMinuteShape,
dayPeriodShape,
dayPeriodBorderSide,
inputDecorationTheme,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
return other is TimePickerThemeData
&& other.backgroundColor == backgroundColor
&& other.hourMinuteTextColor == hourMinuteTextColor
&& other.hourMinuteColor == hourMinuteColor
&& other.dayPeriodTextColor == dayPeriodTextColor
&& other.dayPeriodColor == dayPeriodColor
&& other.dialHandColor == dialHandColor
&& other.dialBackgroundColor == dialBackgroundColor
&& other.entryModeIconColor == entryModeIconColor
&& other.hourMinuteTextStyle == hourMinuteTextStyle
&& other.dayPeriodTextStyle == dayPeriodTextStyle
&& other.helpTextStyle == helpTextStyle
&& other.shape == shape
&& other.hourMinuteShape == hourMinuteShape
&& other.dayPeriodShape == dayPeriodShape
&& other.dayPeriodBorderSide == dayPeriodBorderSide
&& other.inputDecorationTheme == inputDecorationTheme;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(ColorProperty('hourMinuteTextColor', hourMinuteTextColor, defaultValue: null));
properties.add(ColorProperty('hourMinuteColor', hourMinuteColor, defaultValue: null));
properties.add(ColorProperty('dayPeriodTextColor', dayPeriodTextColor, defaultValue: null));
properties.add(ColorProperty('dayPeriodColor', dayPeriodColor, defaultValue: null));
properties.add(ColorProperty('dialHandColor', dialHandColor, defaultValue: null));
properties.add(ColorProperty('dialBackgroundColor', dialBackgroundColor, defaultValue: null));
properties.add(ColorProperty('entryModeIconColor', entryModeIconColor, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('hourMinuteTextStyle', hourMinuteTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('dayPeriodTextStyle', dayPeriodTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<TextStyle>('helpTextStyle', helpTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('hourMinuteShape', hourMinuteShape, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('dayPeriodShape', dayPeriodShape, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('dayPeriodBorderSide', dayPeriodBorderSide, defaultValue: null));
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, defaultValue: null));
}
}
/// An inherited widget that defines the configuration for time pickers
/// displayed using [showTimePicker] in this widget's subtree.
///
/// Values specified here are used for time picker properties that are not
/// given an explicit non-null value.
class TimePickerTheme extends InheritedTheme {
/// Creates a time picker theme that controls the configurations for
/// time pickers displayed in its widget subtree.
const TimePickerTheme({
Key key,
@required this.data,
Widget child,
}) : assert(data != null),
super(key: key, child: child);
/// The properties for descendant time picker widgets.
final TimePickerThemeData data;
/// The [data] value of the closest [TimePickerTheme] ancestor.
///
/// If there is no ancestor, it returns [ThemeData.timePickerTheme].
/// Applications can assume that the returned value will not be null.
///
/// Typical usage is as follows:
///
/// ```dart
/// TimePickerThemeData theme = TimePickerTheme.of(context);
/// ```
static TimePickerThemeData of(BuildContext context) {
final TimePickerTheme timePickerTheme = context.dependOnInheritedWidgetOfExactType<TimePickerTheme>();
return timePickerTheme?.data ?? Theme.of(context).timePickerTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
final TimePickerTheme ancestorTheme = context.findAncestorWidgetOfExactType<TimePickerTheme>();
return identical(this, ancestorTheme) ? child : TimePickerTheme(data: data, child: child);
}
@override
bool updateShouldNotify(TimePickerTheme oldWidget) => data != oldWidget.data;
}

View File

@ -282,6 +282,7 @@ void main() {
dividerTheme: const DividerThemeData(color: Colors.black),
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.start),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.black),
fixTextFieldOutlineLabel: false,
);
@ -363,6 +364,7 @@ void main() {
dividerTheme: const DividerThemeData(color: Colors.white),
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.end),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.shifting),
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.white),
fixTextFieldOutlineLabel: true,
);
@ -430,6 +432,7 @@ void main() {
dividerTheme: otherTheme.dividerTheme,
buttonBarTheme: otherTheme.buttonBarTheme,
bottomNavigationBarTheme: otherTheme.bottomNavigationBarTheme,
timePickerTheme: otherTheme.timePickerTheme,
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
);
@ -499,6 +502,7 @@ void main() {
expect(themeDataCopy.dividerTheme, equals(otherTheme.dividerTheme));
expect(themeDataCopy.buttonBarTheme, equals(otherTheme.buttonBarTheme));
expect(themeDataCopy.bottomNavigationBarTheme, equals(otherTheme.bottomNavigationBarTheme));
expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
});

View File

@ -6,15 +6,12 @@
@TestOn('!chrome') // entire file needs triage.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
@ -23,10 +20,16 @@ final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widge
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog');
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key);
const _TimePickerLauncher({
Key key,
this.onChanged,
this.locale,
this.entryMode = TimePickerEntryMode.dial,
}) : super(key: key);
final ValueChanged<TimeOfDay> onChanged;
final Locale locale;
final TimePickerEntryMode entryMode;
@override
Widget build(BuildContext context) {
@ -35,17 +38,18 @@ class _TimePickerLauncher extends StatelessWidget {
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () async {
onChanged(await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
));
},
);
}
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () async {
onChanged(await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
initialEntryMode: entryMode,
));
},
);
}
),
),
),
@ -53,11 +57,15 @@ class _TimePickerLauncher extends StatelessWidget {
}
}
Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
Future<Offset> startPicker(
WidgetTester tester,
ValueChanged<TimeOfDay> onChanged, {
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
}) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US'), entryMode: entryMode));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
return tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
}
Future<void> finishPicker(WidgetTester tester) async {
@ -67,9 +75,13 @@ Future<void> finishPicker(WidgetTester tester) async {
}
void main() {
group('Time picker', () {
group('Time picker - Dial', () {
_tests();
});
group('Time picker - Input', () {
_testsInput();
});
}
void _tests() {
@ -170,6 +182,34 @@ void _tests() {
expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
});
testWidgets('tap-select rounds down to nearest 5 minute increment', (WidgetTester tester) async {
TimeOfDay result;
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min46 = Offset(center.dx - 50.0, center.dy - 5); // 46 mins
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(min46);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
});
testWidgets('tap-select rounds up to nearest 5 minute increment', (WidgetTester tester) async {
TimeOfDay result;
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
final Offset min48 = Offset(center.dx - 50.0, center.dy - 15); // 48 mins
await tester.tapAt(hour6);
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(min48);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 6, minute: 50)));
});
group('haptic feedback', () {
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
@ -256,64 +296,18 @@ void _tests() {
});
const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
const List<String> labels12To11TwoDigit = <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
Future<void> mediaQueryBoilerplate(
WidgetTester tester,
bool alwaysUse24HourFormat, {
TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
double textScaleFactor = 1.0,
}) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData(
alwaysUse24HourFormat: alwaysUse24HourFormat,
textScaleFactor: textScaleFactor,
),
child: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(builder: (BuildContext context) {
return FlatButton(
onPressed: () {
showTimePicker(context: context, initialTime: initialTime);
},
child: const Text('X'),
);
});
},
),
),
),
),
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
}
const List<String> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, false);
final CustomPaint dialPaint = tester.widget(findDialPaint);
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
expect(dialPainter.primaryInnerLabels, null);
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
expect(dialPainter.secondaryInnerLabels, null);
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
});
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
@ -321,15 +315,11 @@ void _tests() {
final CustomPaint dialPaint = tester.widget(findDialPaint);
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
});
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
@ -347,10 +337,10 @@ void _tests() {
await mediaQueryBoilerplate(tester, true);
expect(semantics, isNot(includesNodeWith(label: ':')));
expect(semantics.nodesWith(value: '00'), hasLength(2),
reason: '00 appears once in the header, then again in the dial');
expect(semantics.nodesWith(value: '07'), hasLength(2),
reason: '07 appears once in the header, then again in the dial');
expect(semantics.nodesWith(value: '00'), hasLength(1),
reason: '00 appears once in the header');
expect(semantics.nodesWith(value: '07'), hasLength(1),
reason: '07 appears once in the header');
expect(semantics, includesNodeWith(label: 'CANCEL'));
expect(semantics, includesNodeWith(label: 'OK'));
@ -361,82 +351,6 @@ void _tests() {
semantics.dispose();
});
testWidgets('provides semantics information for hours', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true);
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final CustomPainter dialPainter = dialPaint.painter;
final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
painterTester.addLabel('13', 129.0, 11.5, 177.0, 59.5);
painterTester.addLabel('14', 160.5, 43.0, 208.5, 91.0);
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
painterTester.addLabel('16', 160.5, 129.0, 208.5, 177.0);
painterTester.addLabel('17', 129.0, 160.5, 177.0, 208.5);
painterTester.addLabel('18', 86.0, 172.0, 134.0, 220.0);
painterTester.addLabel('19', 43.0, 160.5, 91.0, 208.5);
painterTester.addLabel('20', 11.5, 129.0, 59.5, 177.0);
painterTester.addLabel('21', 0.0, 86.0, 48.0, 134.0);
painterTester.addLabel('22', 11.5, 43.0, 59.5, 91.0);
painterTester.addLabel('23', 43.0, 11.5, 91.0, 59.5);
painterTester.addLabel('12', 86.0, 36.0, 134.0, 84.0);
painterTester.addLabel('01', 111.0, 42.7, 159.0, 90.7);
painterTester.addLabel('02', 129.3, 61.0, 177.3, 109.0);
painterTester.addLabel('03', 136.0, 86.0, 184.0, 134.0);
painterTester.addLabel('04', 129.3, 111.0, 177.3, 159.0);
painterTester.addLabel('05', 111.0, 129.3, 159.0, 177.3);
painterTester.addLabel('06', 86.0, 136.0, 134.0, 184.0);
painterTester.addLabel('07', 61.0, 129.3, 109.0, 177.3);
painterTester.addLabel('08', 42.7, 111.0, 90.7, 159.0);
painterTester.addLabel('09', 36.0, 86.0, 84.0, 134.0);
painterTester.addLabel('10', 42.7, 61.0, 90.7, 109.0);
painterTester.addLabel('11', 61.0, 42.7, 109.0, 90.7);
painterTester.assertExpectations();
semantics.dispose();
});
testWidgets('provides semantics information for minutes', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await mediaQueryBoilerplate(tester, true);
await tester.tap(_minuteControl);
await tester.pumpAndSettle();
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final CustomPainter dialPainter = dialPaint.painter;
final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
painterTester.addLabel('05', 129.0, 11.5, 177.0, 59.5);
painterTester.addLabel('10', 160.5, 43.0, 208.5, 91.0);
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
painterTester.addLabel('20', 160.5, 129.0, 208.5, 177.0);
painterTester.addLabel('25', 129.0, 160.5, 177.0, 208.5);
painterTester.addLabel('30', 86.0, 172.0, 134.0, 220.0);
painterTester.addLabel('35', 43.0, 160.5, 91.0, 208.5);
painterTester.addLabel('40', 11.5, 129.0, 59.5, 177.0);
painterTester.addLabel('45', 0.0, 86.0, 48.0, 134.0);
painterTester.addLabel('50', 11.5, 43.0, 59.5, 91.0);
painterTester.addLabel('55', 43.0, 11.5, 91.0, 59.5);
painterTester.assertExpectations();
semantics.dispose();
});
testWidgets('picks the right dial ring from widget configuration', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 12, minute: 0));
dynamic dialPaint = tester.widget(findDialPaint);
expect('${dialPaint.painter.activeRing}', '_DialRing.inner');
await tester.pumpWidget(Container()); // make sure previous state isn't reused
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 0, minute: 0));
dialPaint = tester.widget(findDialPaint);
expect('${dialPaint.painter.activeRing}', '_DialRing.outer');
});
testWidgets('can increment and decrement hours', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@ -550,21 +464,15 @@ void _tests() {
});
testWidgets('header touch regions are large enough', (WidgetTester tester) async {
// Ensure picker is displayed in portrait mode.
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
tester.binding.window.devicePixelRatioTestValue = 1;
await mediaQueryBoilerplate(tester, false);
final Size amSize = tester.getSize(find.ancestor(
of: find.text('AM'),
matching: find.byType(InkWell),
));
expect(amSize.width, greaterThanOrEqualTo(48.0));
expect(amSize.height, greaterThanOrEqualTo(48.0));
final Size pmSize = tester.getSize(find.ancestor(
of: find.text('PM'),
matching: find.byType(InkWell),
));
expect(pmSize.width, greaterThanOrEqualTo(48.0));
expect(pmSize.height, greaterThanOrEqualTo(48.0));
final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
// Height should be double the minimum size to account for both AM/PM stacked.
expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));
final Size hourSize = tester.getSize(find.ancestor(
of: find.text('7'),
@ -579,6 +487,9 @@ void _tests() {
));
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
expect(minuteSize.height, greaterThanOrEqualTo(48.0));
tester.binding.window.physicalSizeTestValue = null;
tester.binding.window.devicePixelRatioTestValue = null;
});
testWidgets('builder parameter', (WidgetTester tester) async {
@ -696,51 +607,158 @@ void _tests() {
expect(nestedObserver.pickerCount, 1);
});
testWidgets('optional text parameters are utilized', (WidgetTester tester) async {
const String cancelText = 'Custom Cancel';
const String confirmText = 'Custom OK';
const String helperText = 'Custom Help';
await tester.pumpWidget(MaterialApp(
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () async {
await showTimePicker(
context: context,
initialTime: const TimeOfDay(hour: 7, minute: 0),
cancelText: cancelText,
confirmText: confirmText,
helpText: helperText,
);
},
);
}
),
),
)
));
// Open the picker.
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.text(cancelText), findsOneWidget);
expect(find.text(confirmText), findsOneWidget);
expect(find.text(helperText), findsOneWidget);
});
// TODO(rami-a): Re-enable and fix test.
testWidgets('text scale affects certain elements and not others',
(WidgetTester tester) async {
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 1.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
(WidgetTester tester) async {
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 1.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
final double amHeight = tester.getSize(find.text('AM')).height;
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
// Verify that the time display is not affected by text scale.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 2.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
final double amHeight2x = tester.getSize(find.text('AM')).height;
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(amHeight2x, greaterThanOrEqualTo(amHeight * 2));
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
// Verify that text scale for AM/PM is at most 2x.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 3.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
});
}
void _testsInput() {
testWidgets('Initial entry mode is used', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
expect(find.byType(TextField), findsNWidgets(2));
});
testWidgets('Initial time is the default', (WidgetTester tester) async {
TimeOfDay result;
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 7, minute: 0)));
});
testWidgets('Help text is used - Input', (WidgetTester tester) async {
const String helpText = 'help';
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, helpText: helpText);
expect(find.text(helpText), findsOneWidget);
});
testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
await tester.tap(find.byIcon(Icons.access_time));
await tester.pumpAndSettle();
expect(find.byType(TextField), findsNothing);
});
final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
final double amHeight = tester.getSize(find.text('AM')).height;
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
testWidgets('Entered text returns time', (WidgetTester tester) async {
TimeOfDay result;
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
await tester.enterText(find.byType(TextField).first, '9');
await tester.enterText(find.byType(TextField).last, '12');
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
});
// Verify that the time display is not affected by text scale.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 2.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
TimeOfDay result;
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '15');
await tester.tap(find.byIcon(Icons.access_time));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
});
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
TimeOfDay result;
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
await tester.tap(find.text('OK')); // dismiss the dialog
await tester.pumpAndSettle();
// Invalid hour.
await tester.enterText(find.byType(TextField).first, '88');
await tester.enterText(find.byType(TextField).last, '15');
await finishPicker(tester);
expect(result, null);
// Verify that text scale for AM/PM is at most 2x.
await mediaQueryBoilerplate(
tester,
false,
textScaleFactor: 3.0,
initialTime: const TimeOfDay(hour: 7, minute: 41),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
// Invalid minute.
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '150');
await finishPicker(tester);
expect(result, null);
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
await tester.enterText(find.byType(TextField).first, '8');
await tester.enterText(find.byType(TextField).last, '15');
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
});
}
@ -749,73 +767,6 @@ final Finder findDialPaint = find.descendant(
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
);
class _SemanticsNodeExpectation {
_SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);
final String label;
final double left;
final double top;
final double right;
final double bottom;
}
class _CustomPainterSemanticsTester {
_CustomPainterSemanticsTester(this.tester, this.painter, this.semantics);
final WidgetTester tester;
final CustomPainter painter;
final SemanticsTester semantics;
final PaintPattern expectedLabels = paints;
final List<_SemanticsNodeExpectation> expectedNodes = <_SemanticsNodeExpectation>[];
void addLabel(String label, double left, double top, double right, double bottom) {
expectedNodes.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
}
void assertExpectations() {
final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
painter.paint(canvasRecording, const Size(220.0, 220.0));
final List<ui.Paragraph> paragraphs = canvasRecording.invocations
.where((RecordedInvocation recordedInvocation) {
return recordedInvocation.invocation.memberName == #drawParagraph;
})
.map<ui.Paragraph>((RecordedInvocation recordedInvocation) {
return recordedInvocation.invocation.positionalArguments.first as ui.Paragraph;
})
.toList();
final PaintPattern expectedLabels = paints;
int i = 0;
for (final _SemanticsNodeExpectation expectation in expectedNodes) {
expect(semantics, includesNodeWith(value: expectation.label));
final Iterable<SemanticsNode> dialLabelNodes = semantics
.nodesWith(value: expectation.label)
.where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
final Rect rect = Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
expect(dialLabelNodes.single.rect, within(distance: 1.0, from: rect),
reason: 'This is checking the node rectangle for label ${expectation.label}');
final ui.Paragraph paragraph = paragraphs[i++];
// The label text paragraph and the semantics node share the same center,
// but have different sizes.
final Offset center = dialLabelNodes.single.rect.center;
final Offset topLeft = center.translate(
-paragraph.width / 2.0,
-paragraph.height / 2.0,
);
expectedLabels.paragraph(
paragraph: paragraph,
offset: within<Offset>(distance: 1.0, from: topLeft),
);
}
expect(tester.renderObject(findDialPaint), expectedLabels);
}
}
class PickerObserver extends NavigatorObserver {
int pickerCount = 0;
@ -827,3 +778,53 @@ class PickerObserver extends NavigatorObserver {
super.didPush(route, previousRoute);
}
}
Future<void> mediaQueryBoilerplate(
WidgetTester tester,
bool alwaysUse24HourFormat, {
TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
double textScaleFactor = 1.0,
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
String helpText,
}) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: MediaQuery(
data: MediaQueryData(
alwaysUse24HourFormat: alwaysUse24HourFormat,
textScaleFactor: textScaleFactor,
),
child: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(builder: (BuildContext context) {
return FlatButton(
onPressed: () {
showTimePicker(
context: context,
initialTime: initialTime,
initialEntryMode: entryMode,
helpText: helpText,
);
},
child: const Text('X'),
);
});
},
),
),
),
),
),
);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
}

View File

@ -0,0 +1,426 @@
// Copyright 2014 The Flutter 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/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
test('TimePickerThemeData copyWith, ==, hashCode basics', () {
expect(const TimePickerThemeData(), const TimePickerThemeData().copyWith());
expect(const TimePickerThemeData().hashCode, const TimePickerThemeData().copyWith().hashCode);
});
test('TimePickerThemeData null fields by default', () {
const TimePickerThemeData timePickerTheme = TimePickerThemeData();
expect(timePickerTheme.backgroundColor, null);
expect(timePickerTheme.hourMinuteTextColor, null);
expect(timePickerTheme.hourMinuteColor, null);
expect(timePickerTheme.dayPeriodTextColor, null);
expect(timePickerTheme.dayPeriodColor, null);
expect(timePickerTheme.dialHandColor, null);
expect(timePickerTheme.dialBackgroundColor, null);
expect(timePickerTheme.dialHandColor, null);
expect(timePickerTheme.dialBackgroundColor, null);
expect(timePickerTheme.entryModeIconColor, null);
expect(timePickerTheme.hourMinuteTextStyle, null);
expect(timePickerTheme.dayPeriodTextStyle, null);
expect(timePickerTheme.helpTextStyle, null);
expect(timePickerTheme.shape, null);
expect(timePickerTheme.hourMinuteShape, null);
expect(timePickerTheme.dayPeriodShape, null);
expect(timePickerTheme.dayPeriodBorderSide, null);
expect(timePickerTheme.inputDecorationTheme, null);
});
testWidgets('Default TimePickerThemeData debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const TimePickerThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('TimePickerThemeData implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const TimePickerThemeData(
backgroundColor: Color(0xFFFFFFFF),
hourMinuteTextColor: Color(0xFFFFFFFF),
hourMinuteColor: Color(0xFFFFFFFF),
dayPeriodTextColor: Color(0xFFFFFFFF),
dayPeriodColor: Color(0xFFFFFFFF),
dialHandColor: Color(0xFFFFFFFF),
dialBackgroundColor: Color(0xFFFFFFFF),
entryModeIconColor: Color(0xFFFFFFFF),
hourMinuteTextStyle: TextStyle(),
dayPeriodTextStyle: TextStyle(),
helpTextStyle: TextStyle(),
shape: RoundedRectangleBorder(),
hourMinuteShape: RoundedRectangleBorder(),
dayPeriodShape: RoundedRectangleBorder(),
dayPeriodBorderSide: BorderSide(),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[
'backgroundColor: Color(0xffffffff)',
'hourMinuteTextColor: Color(0xffffffff)',
'hourMinuteColor: Color(0xffffffff)',
'dayPeriodTextColor: Color(0xffffffff)',
'dayPeriodColor: Color(0xffffffff)',
'dialHandColor: Color(0xffffffff)',
'dialBackgroundColor: Color(0xffffffff)',
'entryModeIconColor: Color(0xffffffff)',
'hourMinuteTextStyle: TextStyle(<all styles inherited>)',
'dayPeriodTextStyle: TextStyle(<all styles inherited>)',
'helpTextStyle: TextStyle(<all styles inherited>)',
'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)',
'hourMinuteShape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)',
'dayPeriodShape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)',
'dayPeriodBorderSide: BorderSide(Color(0xff000000), 1.0, BorderStyle.solid)',
]);
});
testWidgets('Passing no TimePickerThemeData uses defaults', (WidgetTester tester) async {
final ThemeData defaultTheme = ThemeData.fallback();
await tester.pumpWidget(const _TimePickerLauncher());
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Material dialogMaterial = _dialogMaterial(tester);
expect(dialogMaterial.color, defaultTheme.colorScheme.surface);
expect(dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
expect(
dial,
paints
..circle(color: defaultTheme.colorScheme.onBackground.withOpacity(0.12)) // Dial background color.
..circle(color: Color(defaultTheme.colorScheme.primary.value)), // Dial hand color.
);
final RenderParagraph hourText = _textRenderParagraph(tester, '7');
expect(
hourText.text.style,
Typography.material2014().englishLike.headline2
.merge(Typography.material2014().black.headline2)
.copyWith(color: defaultTheme.colorScheme.primary),
);
final RenderParagraph minuteText = _textRenderParagraph(tester, '15');
expect(
minuteText.text.style,
Typography.material2014().englishLike.headline2
.merge(Typography.material2014().black.headline2)
.copyWith(color: defaultTheme.colorScheme.onSurface),
);
final RenderParagraph amText = _textRenderParagraph(tester, 'AM');
expect(
amText.text.style,
Typography.material2014().englishLike.subtitle1
.merge(Typography.material2014().black.subtitle1)
.copyWith(color: defaultTheme.colorScheme.primary),
);
final RenderParagraph pmText = _textRenderParagraph(tester, 'PM');
expect(
pmText.text.style,
Typography.material2014().englishLike.subtitle1
.merge(Typography.material2014().black.subtitle1)
.copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.6)),
);
final RenderParagraph helperText = _textRenderParagraph(tester, 'SELECT TIME');
expect(
helperText.text.style,
Typography.material2014().englishLike.overline
.merge(Typography.material2014().black.overline),
);
final Material hourMaterial = _textMaterial(tester, '7');
expect(hourMaterial.color, defaultTheme.colorScheme.primary.withOpacity(0.12));
expect(hourMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final Material minuteMaterial = _textMaterial(tester, '15');
expect(minuteMaterial.color, defaultTheme.colorScheme.onSurface.withOpacity(0.12));
expect(minuteMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
final Material amMaterial = _textMaterial(tester, 'AM');
expect(amMaterial.color, defaultTheme.colorScheme.primary.withOpacity(0.12));
final Material pmMaterial = _textMaterial(tester, 'PM');
expect(pmMaterial.color, Colors.transparent);
final Color expectedBorderColor = Color.alphaBlend(
defaultTheme.colorScheme.onBackground.withOpacity(0.38),
defaultTheme.colorScheme.surface,
);
final Material dayPeriodMaterial = _dayPeriodMaterial(tester);
expect(
dayPeriodMaterial.shape,
RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
side: BorderSide(color: expectedBorderColor),
),
);
final Container dayPeriodDivider = _dayPeriodDivider(tester);
expect(
dayPeriodDivider.decoration,
BoxDecoration(border: Border(left: BorderSide(color: expectedBorderColor))),
);
final IconButton entryModeIconButton = _entryModeIconButton(tester);
expect(
entryModeIconButton.color,
defaultTheme.colorScheme.onSurface.withOpacity(0.6),
);
});
testWidgets('Passing no TimePickerThemeData uses defaults - input mode', (WidgetTester tester) async {
final ThemeData defaultTheme = ThemeData.fallback();
await tester.pumpWidget(const _TimePickerLauncher(entryMode: TimePickerEntryMode.input));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final InputDecoration hourDecoration = _textField(tester, '7').decoration;
expect(hourDecoration.filled, true);
expect(hourDecoration.fillColor, defaultTheme.colorScheme.onSurface.withOpacity(0.12));
expect(hourDecoration.enabledBorder, const OutlineInputBorder(borderSide: BorderSide(color: Colors.transparent)));
expect(hourDecoration.errorBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)));
expect(hourDecoration.focusedBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.primary, width: 2)));
expect(hourDecoration.focusedErrorBorder, OutlineInputBorder(borderSide: BorderSide(color: defaultTheme.colorScheme.error, width: 2)));
expect(
hourDecoration.hintStyle,
Typography.material2014().englishLike.headline2
.merge(defaultTheme.textTheme.headline2.copyWith(color: defaultTheme.colorScheme.onSurface.withOpacity(0.36))),
);
});
testWidgets('Time picker uses values from TimePickerThemeData', (WidgetTester tester) async {
final TimePickerThemeData timePickerTheme = _timePickerTheme();
final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme);
await tester.pumpWidget(_TimePickerLauncher(themeData: theme,));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final Material dialogMaterial = _dialogMaterial(tester);
expect(dialogMaterial.color, timePickerTheme.backgroundColor);
expect(dialogMaterial.shape, timePickerTheme.shape);
final RenderBox dial = tester.firstRenderObject<RenderBox>(find.byType(CustomPaint));
expect(
dial,
paints
..circle(color: Color(timePickerTheme.dialBackgroundColor.value)) // Dial background color.
..circle(color: Color(timePickerTheme.dialHandColor.value)), // Dial hand color.
);
final RenderParagraph hourText = _textRenderParagraph(tester, '7');
expect(
hourText.text.style,
Typography.material2014().englishLike.bodyText2
.merge(Typography.material2014().black.bodyText2)
.merge(timePickerTheme.hourMinuteTextStyle)
.copyWith(color: _selectedColor),
);
final RenderParagraph minuteText = _textRenderParagraph(tester, '15');
expect(
minuteText.text.style,
Typography.material2014().englishLike.bodyText2
.merge(Typography.material2014().black.bodyText2)
.merge(timePickerTheme.hourMinuteTextStyle)
.copyWith(color: _unselectedColor),
);
final RenderParagraph amText = _textRenderParagraph(tester, 'AM');
expect(
amText.text.style,
Typography.material2014().englishLike.subtitle1
.merge(Typography.material2014().black.subtitle1)
.merge(timePickerTheme.dayPeriodTextStyle)
.copyWith(color: _selectedColor),
);
final RenderParagraph pmText = _textRenderParagraph(tester, 'PM');
expect(
pmText.text.style,
Typography.material2014().englishLike.subtitle1
.merge(Typography.material2014().black.subtitle1)
.merge(timePickerTheme.dayPeriodTextStyle)
.copyWith(color: _unselectedColor),
);
final RenderParagraph helperText = _textRenderParagraph(tester, 'SELECT TIME');
expect(
helperText.text.style,
Typography.material2014().englishLike.bodyText2
.merge(Typography.material2014().black.bodyText2)
.merge(timePickerTheme.helpTextStyle),
);
final Material hourMaterial = _textMaterial(tester, '7');
expect(hourMaterial.color, _selectedColor);
expect(hourMaterial.shape, timePickerTheme.hourMinuteShape);
final Material minuteMaterial = _textMaterial(tester, '15');
expect(minuteMaterial.color, _unselectedColor);
expect(minuteMaterial.shape, timePickerTheme.hourMinuteShape);
final Material amMaterial = _textMaterial(tester, 'AM');
expect(amMaterial.color, _selectedColor);
final Material pmMaterial = _textMaterial(tester, 'PM');
expect(pmMaterial.color, _unselectedColor);
final Material dayPeriodMaterial = _dayPeriodMaterial(tester);
expect(
dayPeriodMaterial.shape,
timePickerTheme.dayPeriodShape.copyWith(side: timePickerTheme.dayPeriodBorderSide),
);
final Container dayPeriodDivider = _dayPeriodDivider(tester);
expect(
dayPeriodDivider.decoration,
BoxDecoration(border: Border(left: timePickerTheme.dayPeriodBorderSide)),
);
final IconButton entryModeIconButton = _entryModeIconButton(tester);
expect(
entryModeIconButton.color,
timePickerTheme.entryModeIconColor,
);
});
testWidgets('Time picker uses values from TimePickerThemeData - input mode', (WidgetTester tester) async {
final TimePickerThemeData timePickerTheme = _timePickerTheme();
final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme);
await tester.pumpWidget(_TimePickerLauncher(themeData: theme, entryMode: TimePickerEntryMode.input));
await tester.tap(find.text('X'));
await tester.pumpAndSettle(const Duration(seconds: 1));
final InputDecoration hourDecoration = _textField(tester, '7').decoration;
expect(hourDecoration.filled, timePickerTheme.inputDecorationTheme.filled);
expect(hourDecoration.fillColor, timePickerTheme.inputDecorationTheme.fillColor);
expect(hourDecoration.enabledBorder, timePickerTheme.inputDecorationTheme.enabledBorder);
expect(hourDecoration.errorBorder, timePickerTheme.inputDecorationTheme.errorBorder);
expect(hourDecoration.focusedBorder, timePickerTheme.inputDecorationTheme.focusedBorder);
expect(hourDecoration.focusedErrorBorder, timePickerTheme.inputDecorationTheme.focusedErrorBorder);
expect(hourDecoration.hintStyle, timePickerTheme.inputDecorationTheme.hintStyle);
});
}
final Color _selectedColor = Colors.green[100];
final Color _unselectedColor = Colors.green[200];
TimePickerThemeData _timePickerTheme() {
Color getColor(Set<MaterialState> states) {
return states.contains(MaterialState.selected) ? _selectedColor : _unselectedColor;
}
final MaterialStateColor materialStateColor = MaterialStateColor.resolveWith(getColor);
return TimePickerThemeData(
backgroundColor: Colors.orange,
hourMinuteTextColor: materialStateColor,
hourMinuteColor: materialStateColor,
dayPeriodTextColor: materialStateColor,
dayPeriodColor: materialStateColor,
dialHandColor: Colors.brown,
dialBackgroundColor: Colors.pinkAccent,
entryModeIconColor: Colors.red,
hourMinuteTextStyle: const TextStyle(fontSize: 8.0),
dayPeriodTextStyle: const TextStyle(fontSize: 8.0),
helpTextStyle: const TextStyle(fontSize: 8.0),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
hourMinuteShape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
dayPeriodShape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))),
dayPeriodBorderSide: const BorderSide(color: Colors.blueAccent),
inputDecorationTheme: const InputDecorationTheme(
filled: true,
fillColor: Colors.purple,
enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.blue)),
errorBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.green)),
focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.yellow)),
focusedErrorBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.red)),
hintStyle: TextStyle(fontSize: 8),
),
);
}
class _TimePickerLauncher extends StatelessWidget {
const _TimePickerLauncher({
Key key,
this.themeData,
this.entryMode = TimePickerEntryMode.dial,
}) : super(key: key);
final ThemeData themeData;
final TimePickerEntryMode entryMode;
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: Builder(
builder: (BuildContext context) {
return RaisedButton(
child: const Text('X'),
onPressed: () async {
await showTimePicker(
context: context,
initialEntryMode: entryMode,
initialTime: const TimeOfDay(hour: 7, minute: 15),
);
},
);
}
),
),
),
);
}
}
Material _dialogMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: find.byType(Dialog), matching: find.byType(Material)).first);
}
Material _textMaterial(WidgetTester tester, String text) {
return tester.widget<Material>(find.ancestor(of: find.text(text), matching: find.byType(Material)).first);
}
TextField _textField(WidgetTester tester, String text) {
return tester.widget<TextField>(find.ancestor(of: find.text(text), matching: find.byType(TextField)).first);
}
Material _dayPeriodMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'), matching: find.byType(Material)).first);
}
Container _dayPeriodDivider(WidgetTester tester) {
return tester.widget<Container>(find.descendant(of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'), matching: find.byType(Container)).at(1));
}
IconButton _entryModeIconButton(WidgetTester tester) {
return tester.widget<IconButton>(find.descendant(of: find.byType(Dialog), matching: find.byType(IconButton)).first);
}
RenderParagraph _textRenderParagraph(WidgetTester tester, String text) {
return tester.element<StatelessElement>(find.text(text).first).renderObject as RenderParagraph;
}

View File

@ -43,7 +43,7 @@ class _TimePickerLauncher extends StatelessWidget {
Future<Offset> startPicker(
WidgetTester tester,
ValueChanged<TimeOfDay> onChanged, {
Locale locale = const Locale('en', 'US'),
Locale locale = const Locale('en', 'US'),
}) async {
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: locale,));
await tester.tap(find.text('X'));
@ -58,66 +58,151 @@ Future<void> finishPicker(WidgetTester tester) async {
}
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', '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'
};
testWidgets('can localize the header in all known formats - portrait', (WidgetTester tester) async {
// Ensure picker is displayed in portrait mode.
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
tester.binding.window.devicePixelRatioTestValue = 1;
for (final Locale locale in locales.keys) {
final Finder stringFragmentTextFinder = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
matching: find.byType(Text),
).first;
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteControl');
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
// 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 List<Locale> locales = <Locale>[
const Locale('en', 'US'), //'h:mm a'
const Locale('en', 'GB'), //'HH:mm'
const Locale('es', 'ES'), //'H:mm'
const Locale('fr', 'CA'), //'HH \'h\' mm'
const Locale('zh', 'ZH'), //'ah:mm'
];
for (final Locale locale in locales) {
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 as LayoutId;
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');
} else if (fragmentType == '_StringFragment') {
actual.add('string ${widget.value}');
} else {
fail('Unsupported fragment type: $fragmentType');
}
});
expect(actual, locales[locale]);
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
if (locale == const Locale('en', 'US')) {
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(minuteLeftOffset, lessThan(dayPeriodLeftOffset));
} else if (locale == const Locale('en', 'GB')) {
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('es', 'ES')) {
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('fr', 'CA')) {
expect(stringFragmentText.data, 'h');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('zh', 'ZH')) {
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
expect(stringFragmentText.data, ':');
expect(dayPeriodLeftOffset, lessThan(hourLeftOffset));
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
}
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
}
tester.binding.window.physicalSizeTestValue = null;
tester.binding.window.devicePixelRatioTestValue = null;
});
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(Offset(center.dx, center.dy - dy));
testWidgets('can localize the header in all known formats - landscape', (WidgetTester tester) async {
// Ensure picker is displayed in landscape mode.
tester.binding.window.physicalSizeTestValue = const Size(800, 400);
tester.binding.window.devicePixelRatioTestValue = 1;
final Finder stringFragmentTextFinder = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
matching: find.byType(Text),
).first;
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteControl');
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
// 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 List<Locale> locales = <Locale>[
const Locale('en', 'US'), //'h:mm a'
const Locale('en', 'GB'), //'HH:mm'
const Locale('es', 'ES'), //'H:mm'
const Locale('fr', 'CA'), //'HH \'h\' mm'
const Locale('zh', 'ZH'), //'ah:mm'
];
for (final Locale locale in locales) {
final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale);
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
final double hourTopOffset = tester.getTopLeft(hourControlFinder).dy;
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
if (locale == const Locale('en', 'US')) {
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
final double dayPeriodTopOffset = tester.getTopLeft(dayPeriodControlFinder).dy;
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(hourLeftOffset, dayPeriodLeftOffset);
expect(hourTopOffset, lessThan(dayPeriodTopOffset));
} else if (locale == const Locale('en', 'GB')) {
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('es', 'ES')) {
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('fr', 'CA')) {
expect(stringFragmentText.data, 'h');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(dayPeriodControlFinder, findsNothing);
} else if (locale == const Locale('zh', 'ZH')) {
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
final double dayPeriodTopOffset = tester.getTopLeft(dayPeriodControlFinder).dy;
expect(stringFragmentText.data, ':');
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
expect(hourLeftOffset, dayPeriodLeftOffset);
expect(hourTopOffset, greaterThan(dayPeriodTopOffset));
}
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
await finishPicker(tester);
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
}
tester.binding.window.physicalSizeTestValue = null;
tester.binding.window.devicePixelRatioTestValue = null;
});
testWidgets('uses two-ring 24-hour dial for H and HH hour formats', (WidgetTester tester) async {
testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async {
const List<Locale> locales = <Locale>[
Locale('en', 'US'), // h
Locale('en', 'GB'), // HH
Locale('es', 'ES'), // H
];
for (final 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.
// 12:00 AM position. Because there's only one ring, no matter where you
// tap the time will be the same.
for (int i = 1; i < 10; i++) {
TimeOfDay result;
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale);
@ -125,14 +210,13 @@ void main() {
final double dy = (size.height / 2.0 / 10) * i;
await tester.tapAt(Offset(center.dx, center.dy - dy));
await finishPicker(tester);
expect(result, equals(TimeOfDay(hour: i < 7 ? 12 : 0, minute: 0)));
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
}
}
});
const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
const List<String> labels12To11TwoDigit = <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
const List<String> labels00To22TwoDigit = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
Future<void> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
await tester.pumpWidget(
@ -174,19 +258,17 @@ void main() {
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(
primaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
primaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels12To11,
);
expect(dialPainter.primaryInnerLabels, null);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(
secondaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels12To11,
);
expect(dialPainter.secondaryInnerLabels, null);
});
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
@ -194,26 +276,16 @@ void main() {
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
final dynamic dialPainter = dialPaint.painter;
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
expect(
primaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels00To23,
);
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
expect(
primaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels12To11TwoDigit,
primaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels00To22TwoDigit,
);
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
expect(
secondaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels00To23,
);
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
expect(
secondaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels12To11TwoDigit,
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
labels00To22TwoDigit,
);
});
}