mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[Material] Redesign Time Picker (#59191)
This commit is contained in:
parent
2cd205bb5a
commit
e676024d37
@ -124,6 +124,7 @@ export 'src/material/theme.dart';
|
|||||||
export 'src/material/theme_data.dart';
|
export 'src/material/theme_data.dart';
|
||||||
export 'src/material/time.dart';
|
export 'src/material/time.dart';
|
||||||
export 'src/material/time_picker.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.dart';
|
||||||
export 'src/material/toggle_buttons_theme.dart';
|
export 'src/material/toggle_buttons_theme.dart';
|
||||||
export 'src/material/toggleable.dart';
|
export 'src/material/toggleable.dart';
|
||||||
|
@ -35,6 +35,7 @@ import 'slider_theme.dart';
|
|||||||
import 'snack_bar_theme.dart';
|
import 'snack_bar_theme.dart';
|
||||||
import 'tab_bar_theme.dart';
|
import 'tab_bar_theme.dart';
|
||||||
import 'text_theme.dart';
|
import 'text_theme.dart';
|
||||||
|
import 'time_picker_theme.dart';
|
||||||
import 'toggle_buttons_theme.dart';
|
import 'toggle_buttons_theme.dart';
|
||||||
import 'tooltip_theme.dart';
|
import 'tooltip_theme.dart';
|
||||||
import 'typography.dart';
|
import 'typography.dart';
|
||||||
@ -269,6 +270,7 @@ class ThemeData with Diagnosticable {
|
|||||||
DividerThemeData dividerTheme,
|
DividerThemeData dividerTheme,
|
||||||
ButtonBarThemeData buttonBarTheme,
|
ButtonBarThemeData buttonBarTheme,
|
||||||
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
||||||
|
TimePickerThemeData timePickerTheme,
|
||||||
bool fixTextFieldOutlineLabel,
|
bool fixTextFieldOutlineLabel,
|
||||||
}) {
|
}) {
|
||||||
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
|
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
|
||||||
@ -380,6 +382,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dividerTheme ??= const DividerThemeData();
|
dividerTheme ??= const DividerThemeData();
|
||||||
buttonBarTheme ??= const ButtonBarThemeData();
|
buttonBarTheme ??= const ButtonBarThemeData();
|
||||||
bottomNavigationBarTheme ??= const BottomNavigationBarThemeData();
|
bottomNavigationBarTheme ??= const BottomNavigationBarThemeData();
|
||||||
|
timePickerTheme ??= const TimePickerThemeData();
|
||||||
|
|
||||||
fixTextFieldOutlineLabel ??= false;
|
fixTextFieldOutlineLabel ??= false;
|
||||||
|
|
||||||
@ -448,6 +451,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dividerTheme: dividerTheme,
|
dividerTheme: dividerTheme,
|
||||||
buttonBarTheme: buttonBarTheme,
|
buttonBarTheme: buttonBarTheme,
|
||||||
bottomNavigationBarTheme: bottomNavigationBarTheme,
|
bottomNavigationBarTheme: bottomNavigationBarTheme,
|
||||||
|
timePickerTheme: timePickerTheme,
|
||||||
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
|
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -527,6 +531,7 @@ class ThemeData with Diagnosticable {
|
|||||||
@required this.dividerTheme,
|
@required this.dividerTheme,
|
||||||
@required this.buttonBarTheme,
|
@required this.buttonBarTheme,
|
||||||
@required this.bottomNavigationBarTheme,
|
@required this.bottomNavigationBarTheme,
|
||||||
|
@required this.timePickerTheme,
|
||||||
@required this.fixTextFieldOutlineLabel,
|
@required this.fixTextFieldOutlineLabel,
|
||||||
}) : assert(visualDensity != null),
|
}) : assert(visualDensity != null),
|
||||||
assert(primaryColor != null),
|
assert(primaryColor != null),
|
||||||
@ -589,6 +594,7 @@ class ThemeData with Diagnosticable {
|
|||||||
assert(dividerTheme != null),
|
assert(dividerTheme != null),
|
||||||
assert(buttonBarTheme != null),
|
assert(buttonBarTheme != null),
|
||||||
assert(bottomNavigationBarTheme != null),
|
assert(bottomNavigationBarTheme != null),
|
||||||
|
assert(timePickerTheme != null),
|
||||||
assert(fixTextFieldOutlineLabel != null);
|
assert(fixTextFieldOutlineLabel != null);
|
||||||
|
|
||||||
/// Create a [ThemeData] based on the colors in the given [colorScheme] and
|
/// Create a [ThemeData] based on the colors in the given [colorScheme] and
|
||||||
@ -1036,6 +1042,9 @@ class ThemeData with Diagnosticable {
|
|||||||
/// widgets.
|
/// widgets.
|
||||||
final BottomNavigationBarThemeData bottomNavigationBarTheme;
|
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
|
/// A temporary flag to allow apps to opt-in to a
|
||||||
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
|
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
|
||||||
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
|
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
|
||||||
@ -1117,6 +1126,7 @@ class ThemeData with Diagnosticable {
|
|||||||
DividerThemeData dividerTheme,
|
DividerThemeData dividerTheme,
|
||||||
ButtonBarThemeData buttonBarTheme,
|
ButtonBarThemeData buttonBarTheme,
|
||||||
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
||||||
|
TimePickerThemeData timePickerTheme,
|
||||||
bool fixTextFieldOutlineLabel,
|
bool fixTextFieldOutlineLabel,
|
||||||
}) {
|
}) {
|
||||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||||
@ -1185,6 +1195,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dividerTheme: dividerTheme ?? this.dividerTheme,
|
dividerTheme: dividerTheme ?? this.dividerTheme,
|
||||||
buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme,
|
buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme,
|
||||||
bottomNavigationBarTheme: bottomNavigationBarTheme ?? this.bottomNavigationBarTheme,
|
bottomNavigationBarTheme: bottomNavigationBarTheme ?? this.bottomNavigationBarTheme,
|
||||||
|
timePickerTheme: timePickerTheme ?? this.timePickerTheme,
|
||||||
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel,
|
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1331,6 +1342,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
||||||
buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t),
|
buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t),
|
||||||
bottomNavigationBarTheme: BottomNavigationBarThemeData.lerp(a.bottomNavigationBarTheme, b.bottomNavigationBarTheme, 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,
|
fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1405,6 +1417,7 @@ class ThemeData with Diagnosticable {
|
|||||||
&& other.dividerTheme == dividerTheme
|
&& other.dividerTheme == dividerTheme
|
||||||
&& other.buttonBarTheme == buttonBarTheme
|
&& other.buttonBarTheme == buttonBarTheme
|
||||||
&& other.bottomNavigationBarTheme == bottomNavigationBarTheme
|
&& other.bottomNavigationBarTheme == bottomNavigationBarTheme
|
||||||
|
&& other.timePickerTheme == timePickerTheme
|
||||||
&& other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel;
|
&& other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1478,6 +1491,7 @@ class ThemeData with Diagnosticable {
|
|||||||
dividerTheme,
|
dividerTheme,
|
||||||
buttonBarTheme,
|
buttonBarTheme,
|
||||||
bottomNavigationBarTheme,
|
bottomNavigationBarTheme,
|
||||||
|
timePickerTheme,
|
||||||
fixTextFieldOutlineLabel,
|
fixTextFieldOutlineLabel,
|
||||||
];
|
];
|
||||||
return hashList(values);
|
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<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));
|
||||||
properties.add(DiagnosticsProperty<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, 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<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));
|
properties.add(DiagnosticsProperty<BottomNavigationBarThemeData>('bottomNavigationBarTheme', bottomNavigationBarTheme, defaultValue: defaultData.bottomNavigationBarTheme, level: DiagnosticLevel.debug));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
381
packages/flutter/lib/src/material/time_picker_theme.dart
Normal file
381
packages/flutter/lib/src/material/time_picker_theme.dart
Normal 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;
|
||||||
|
}
|
@ -282,6 +282,7 @@ void main() {
|
|||||||
dividerTheme: const DividerThemeData(color: Colors.black),
|
dividerTheme: const DividerThemeData(color: Colors.black),
|
||||||
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.start),
|
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.start),
|
||||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
||||||
|
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.black),
|
||||||
fixTextFieldOutlineLabel: false,
|
fixTextFieldOutlineLabel: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -363,6 +364,7 @@ void main() {
|
|||||||
dividerTheme: const DividerThemeData(color: Colors.white),
|
dividerTheme: const DividerThemeData(color: Colors.white),
|
||||||
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.end),
|
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.end),
|
||||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.shifting),
|
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.shifting),
|
||||||
|
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.white),
|
||||||
fixTextFieldOutlineLabel: true,
|
fixTextFieldOutlineLabel: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -430,6 +432,7 @@ void main() {
|
|||||||
dividerTheme: otherTheme.dividerTheme,
|
dividerTheme: otherTheme.dividerTheme,
|
||||||
buttonBarTheme: otherTheme.buttonBarTheme,
|
buttonBarTheme: otherTheme.buttonBarTheme,
|
||||||
bottomNavigationBarTheme: otherTheme.bottomNavigationBarTheme,
|
bottomNavigationBarTheme: otherTheme.bottomNavigationBarTheme,
|
||||||
|
timePickerTheme: otherTheme.timePickerTheme,
|
||||||
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -499,6 +502,7 @@ void main() {
|
|||||||
expect(themeDataCopy.dividerTheme, equals(otherTheme.dividerTheme));
|
expect(themeDataCopy.dividerTheme, equals(otherTheme.dividerTheme));
|
||||||
expect(themeDataCopy.buttonBarTheme, equals(otherTheme.buttonBarTheme));
|
expect(themeDataCopy.buttonBarTheme, equals(otherTheme.buttonBarTheme));
|
||||||
expect(themeDataCopy.bottomNavigationBarTheme, equals(otherTheme.bottomNavigationBarTheme));
|
expect(themeDataCopy.bottomNavigationBarTheme, equals(otherTheme.bottomNavigationBarTheme));
|
||||||
|
expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
|
||||||
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,15 +6,12 @@
|
|||||||
|
|
||||||
@TestOn('!chrome') // entire file needs triage.
|
@TestOn('!chrome') // entire file needs triage.
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../rendering/mock_canvas.dart';
|
|
||||||
import '../rendering/recording_canvas.dart';
|
|
||||||
import '../widgets/semantics_tester.dart';
|
import '../widgets/semantics_tester.dart';
|
||||||
import 'feedback_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');
|
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog');
|
||||||
|
|
||||||
class _TimePickerLauncher extends StatelessWidget {
|
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 ValueChanged<TimeOfDay> onChanged;
|
||||||
final Locale locale;
|
final Locale locale;
|
||||||
|
final TimePickerEntryMode entryMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -35,17 +38,18 @@ class _TimePickerLauncher extends StatelessWidget {
|
|||||||
home: Material(
|
home: Material(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return RaisedButton(
|
return RaisedButton(
|
||||||
child: const Text('X'),
|
child: const Text('X'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
onChanged(await showTimePicker(
|
onChanged(await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
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 {
|
Future<Offset> startPicker(
|
||||||
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
|
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.tap(find.text('X'));
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
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 {
|
Future<void> finishPicker(WidgetTester tester) async {
|
||||||
@ -67,9 +75,13 @@ Future<void> finishPicker(WidgetTester tester) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Time picker', () {
|
group('Time picker - Dial', () {
|
||||||
_tests();
|
_tests();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Time picker - Input', () {
|
||||||
|
_testsInput();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _tests() {
|
void _tests() {
|
||||||
@ -170,6 +182,34 @@ void _tests() {
|
|||||||
expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
|
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', () {
|
group('haptic feedback', () {
|
||||||
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
|
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
|
||||||
const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
|
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> 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> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(tester, false);
|
await mediaQueryBoilerplate(tester, false);
|
||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
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.text.text as String), labels12To11);
|
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), 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.text.text as String), labels12To11);
|
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
|
||||||
expect(dialPainter.secondaryInnerLabels, null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
||||||
@ -321,15 +315,11 @@ void _tests() {
|
|||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
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.text.text as String), labels00To23);
|
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
|
||||||
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> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||||
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
|
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
|
||||||
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
|
|
||||||
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
|
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
|
||||||
@ -347,10 +337,10 @@ void _tests() {
|
|||||||
await mediaQueryBoilerplate(tester, true);
|
await mediaQueryBoilerplate(tester, true);
|
||||||
|
|
||||||
expect(semantics, isNot(includesNodeWith(label: ':')));
|
expect(semantics, isNot(includesNodeWith(label: ':')));
|
||||||
expect(semantics.nodesWith(value: '00'), hasLength(2),
|
expect(semantics.nodesWith(value: '00'), hasLength(1),
|
||||||
reason: '00 appears once in the header, then again in the dial');
|
reason: '00 appears once in the header');
|
||||||
expect(semantics.nodesWith(value: '07'), hasLength(2),
|
expect(semantics.nodesWith(value: '07'), hasLength(1),
|
||||||
reason: '07 appears once in the header, then again in the dial');
|
reason: '07 appears once in the header');
|
||||||
expect(semantics, includesNodeWith(label: 'CANCEL'));
|
expect(semantics, includesNodeWith(label: 'CANCEL'));
|
||||||
expect(semantics, includesNodeWith(label: 'OK'));
|
expect(semantics, includesNodeWith(label: 'OK'));
|
||||||
|
|
||||||
@ -361,82 +351,6 @@ void _tests() {
|
|||||||
semantics.dispose();
|
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 {
|
testWidgets('can increment and decrement hours', (WidgetTester tester) async {
|
||||||
final SemanticsTester semantics = SemanticsTester(tester);
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
|
||||||
@ -550,21 +464,15 @@ void _tests() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('header touch regions are large enough', (WidgetTester tester) async {
|
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);
|
await mediaQueryBoilerplate(tester, false);
|
||||||
|
|
||||||
final Size amSize = tester.getSize(find.ancestor(
|
final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
|
||||||
of: find.text('AM'),
|
expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
|
||||||
matching: find.byType(InkWell),
|
// Height should be double the minimum size to account for both AM/PM stacked.
|
||||||
));
|
expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));
|
||||||
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 hourSize = tester.getSize(find.ancestor(
|
final Size hourSize = tester.getSize(find.ancestor(
|
||||||
of: find.text('7'),
|
of: find.text('7'),
|
||||||
@ -579,6 +487,9 @@ void _tests() {
|
|||||||
));
|
));
|
||||||
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
|
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
|
||||||
expect(minuteSize.height, 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 {
|
testWidgets('builder parameter', (WidgetTester tester) async {
|
||||||
@ -696,51 +607,158 @@ void _tests() {
|
|||||||
expect(nestedObserver.pickerCount, 1);
|
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',
|
testWidgets('text scale affects certain elements and not others',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(
|
await mediaQueryBoilerplate(
|
||||||
tester,
|
tester,
|
||||||
false,
|
false,
|
||||||
textScaleFactor: 1.0,
|
textScaleFactor: 1.0,
|
||||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||||
);
|
);
|
||||||
await tester.tap(find.text('X'));
|
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();
|
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
|
testWidgets('Entered text returns time', (WidgetTester tester) async {
|
||||||
await tester.pumpAndSettle();
|
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.
|
testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
|
||||||
await mediaQueryBoilerplate(
|
TimeOfDay result;
|
||||||
tester,
|
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||||
false,
|
await tester.enterText(find.byType(TextField).first, '8');
|
||||||
textScaleFactor: 2.0,
|
await tester.enterText(find.byType(TextField).last, '15');
|
||||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
await tester.tap(find.byIcon(Icons.access_time));
|
||||||
);
|
await finishPicker(tester);
|
||||||
await tester.tap(find.text('X'));
|
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
|
||||||
await tester.pumpAndSettle();
|
});
|
||||||
|
|
||||||
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
|
testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
|
||||||
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
|
TimeOfDay result;
|
||||||
|
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||||
|
|
||||||
await tester.tap(find.text('OK')); // dismiss the dialog
|
// Invalid hour.
|
||||||
await tester.pumpAndSettle();
|
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.
|
// Invalid minute.
|
||||||
await mediaQueryBoilerplate(
|
await tester.enterText(find.byType(TextField).first, '8');
|
||||||
tester,
|
await tester.enterText(find.byType(TextField).last, '150');
|
||||||
false,
|
await finishPicker(tester);
|
||||||
textScaleFactor: 3.0,
|
expect(result, null);
|
||||||
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));
|
await tester.enterText(find.byType(TextField).first, '8');
|
||||||
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
|
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),
|
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 {
|
class PickerObserver extends NavigatorObserver {
|
||||||
int pickerCount = 0;
|
int pickerCount = 0;
|
||||||
|
|
||||||
@ -827,3 +778,53 @@ class PickerObserver extends NavigatorObserver {
|
|||||||
super.didPush(route, previousRoute);
|
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();
|
||||||
|
}
|
||||||
|
426
packages/flutter/test/material/time_picker_theme_test.dart
Normal file
426
packages/flutter/test/material/time_picker_theme_test.dart
Normal 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;
|
||||||
|
}
|
@ -43,7 +43,7 @@ class _TimePickerLauncher extends StatelessWidget {
|
|||||||
Future<Offset> startPicker(
|
Future<Offset> startPicker(
|
||||||
WidgetTester tester,
|
WidgetTester tester,
|
||||||
ValueChanged<TimeOfDay> onChanged, {
|
ValueChanged<TimeOfDay> onChanged, {
|
||||||
Locale locale = const Locale('en', 'US'),
|
Locale locale = const Locale('en', 'US'),
|
||||||
}) async {
|
}) async {
|
||||||
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: locale,));
|
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: locale,));
|
||||||
await tester.tap(find.text('X'));
|
await tester.tap(find.text('X'));
|
||||||
@ -58,66 +58,151 @@ Future<void> finishPicker(WidgetTester tester) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('can localize the header in all known formats', (WidgetTester tester) async {
|
testWidgets('can localize the header in all known formats - portrait', (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
|
// Ensure picker is displayed in portrait mode.
|
||||||
final Map<Locale, List<String>> locales = <Locale, List<String>>{
|
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
|
||||||
const Locale('en', 'US'): const <String>['hour', 'string :', 'minute', 'period'], //'h:mm a'
|
tester.binding.window.devicePixelRatioTestValue = 1;
|
||||||
const Locale('en', 'GB'): const <String>['hour', 'string :', 'minute'], //'HH:mm'
|
|
||||||
const Locale('es', 'ES'): const <String>['hour', 'string :', 'minute'], //'H:mm'
|
|
||||||
const Locale('fr', 'CA'): const <String>['hour', 'string h', 'minute'], //'HH \'h\' mm'
|
|
||||||
const Locale('zh', 'ZH'): const <String>['period', 'hour', 'string :', 'minute'], //'ah:mm'
|
|
||||||
};
|
|
||||||
|
|
||||||
for (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 Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale);
|
||||||
final List<String> actual = <String>[];
|
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
|
||||||
tester.element(find.byType(CustomMultiChildLayout)).visitChildren((Element child) {
|
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
|
||||||
final LayoutId layout = child.widget as LayoutId;
|
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
|
||||||
final String fragmentType = '${layout.child.runtimeType}';
|
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
|
||||||
final dynamic widget = layout.child;
|
|
||||||
if (fragmentType == '_MinuteControl') {
|
if (locale == const Locale('en', 'US')) {
|
||||||
actual.add('minute');
|
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||||
} else if (fragmentType == '_DayPeriodControl') {
|
expect(stringFragmentText.data, ':');
|
||||||
actual.add('period');
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
} else if (fragmentType == '_HourControl') {
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
actual.add('hour');
|
expect(minuteLeftOffset, lessThan(dayPeriodLeftOffset));
|
||||||
} else if (fragmentType == '_StringFragment') {
|
} else if (locale == const Locale('en', 'GB')) {
|
||||||
actual.add('string ${widget.value}');
|
expect(stringFragmentText.data, ':');
|
||||||
} else {
|
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||||
fail('Unsupported fragment type: $fragmentType');
|
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||||
}
|
expect(dayPeriodControlFinder, findsNothing);
|
||||||
});
|
} else if (locale == const Locale('es', 'ES')) {
|
||||||
expect(actual, locales[locale]);
|
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 tester.tapAt(Offset(center.dx, center.dy - 50.0));
|
||||||
await finishPicker(tester);
|
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 {
|
testWidgets('can localize the header in all known formats - landscape', (WidgetTester tester) async {
|
||||||
// Tap along the segment stretching from the center to the edge at
|
// Ensure picker is displayed in landscape mode.
|
||||||
// 12:00 AM position. Because there's only one ring, no matter where you
|
tester.binding.window.physicalSizeTestValue = const Size(800, 400);
|
||||||
// tap the time will be the same. See the 24-hour dial test that behaves
|
tester.binding.window.devicePixelRatioTestValue = 1;
|
||||||
// differently.
|
|
||||||
for (int i = 1; i < 10; i++) {
|
final Finder stringFragmentTextFinder = find.descendant(
|
||||||
TimeOfDay result;
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
|
||||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
|
matching: find.byType(Text),
|
||||||
final Size size = tester.getSize(find.byKey(const Key('time-picker-dial')));
|
).first;
|
||||||
final double dy = (size.height / 2.0 / 10) * i;
|
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
|
||||||
await tester.tapAt(Offset(center.dx, center.dy - dy));
|
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);
|
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>[
|
const List<Locale> locales = <Locale>[
|
||||||
|
Locale('en', 'US'), // h
|
||||||
Locale('en', 'GB'), // HH
|
Locale('en', 'GB'), // HH
|
||||||
Locale('es', 'ES'), // H
|
Locale('es', 'ES'), // H
|
||||||
];
|
];
|
||||||
for (final Locale locale in locales) {
|
for (final Locale locale in locales) {
|
||||||
// Tap along the segment stretching from the center to the edge at
|
// 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
|
// 12:00 AM position. Because there's only one ring, no matter where you
|
||||||
// switches between inner ring and outer ring.
|
// tap the time will be the same.
|
||||||
for (int i = 1; i < 10; i++) {
|
for (int i = 1; i < 10; i++) {
|
||||||
TimeOfDay result;
|
TimeOfDay result;
|
||||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale);
|
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;
|
final double dy = (size.height / 2.0 / 10) * i;
|
||||||
await tester.tapAt(Offset(center.dx, center.dy - dy));
|
await tester.tapAt(Offset(center.dx, center.dy - dy));
|
||||||
await finishPicker(tester);
|
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> 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> labels00To22TwoDigit = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
|
||||||
const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
|
||||||
|
|
||||||
Future<void> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
Future<void> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -174,19 +258,17 @@ void main() {
|
|||||||
|
|
||||||
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||||
expect(
|
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,
|
labels12To11,
|
||||||
);
|
);
|
||||||
expect(dialPainter.primaryInnerLabels, null);
|
|
||||||
|
|
||||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||||
expect(
|
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,
|
labels12To11,
|
||||||
);
|
);
|
||||||
expect(dialPainter.secondaryInnerLabels, null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
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 CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||||
final dynamic dialPainter = dialPaint.painter;
|
final dynamic dialPainter = dialPaint.painter;
|
||||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||||
expect(
|
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),
|
||||||
labels00To23,
|
labels00To22TwoDigit,
|
||||||
);
|
|
||||||
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
|
|
||||||
expect(
|
|
||||||
primaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
|
||||||
labels12To11TwoDigit,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||||
expect(
|
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),
|
||||||
labels00To23,
|
labels00To22TwoDigit,
|
||||||
);
|
|
||||||
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
|
|
||||||
expect(
|
|
||||||
secondaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
|
||||||
labels12To11TwoDigit,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user