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/time.dart';
|
||||
export 'src/material/time_picker.dart';
|
||||
export 'src/material/time_picker_theme.dart';
|
||||
export 'src/material/toggle_buttons.dart';
|
||||
export 'src/material/toggle_buttons_theme.dart';
|
||||
export 'src/material/toggleable.dart';
|
||||
|
@ -35,6 +35,7 @@ import 'slider_theme.dart';
|
||||
import 'snack_bar_theme.dart';
|
||||
import 'tab_bar_theme.dart';
|
||||
import 'text_theme.dart';
|
||||
import 'time_picker_theme.dart';
|
||||
import 'toggle_buttons_theme.dart';
|
||||
import 'tooltip_theme.dart';
|
||||
import 'typography.dart';
|
||||
@ -269,6 +270,7 @@ class ThemeData with Diagnosticable {
|
||||
DividerThemeData dividerTheme,
|
||||
ButtonBarThemeData buttonBarTheme,
|
||||
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
||||
TimePickerThemeData timePickerTheme,
|
||||
bool fixTextFieldOutlineLabel,
|
||||
}) {
|
||||
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
|
||||
@ -380,6 +382,7 @@ class ThemeData with Diagnosticable {
|
||||
dividerTheme ??= const DividerThemeData();
|
||||
buttonBarTheme ??= const ButtonBarThemeData();
|
||||
bottomNavigationBarTheme ??= const BottomNavigationBarThemeData();
|
||||
timePickerTheme ??= const TimePickerThemeData();
|
||||
|
||||
fixTextFieldOutlineLabel ??= false;
|
||||
|
||||
@ -448,6 +451,7 @@ class ThemeData with Diagnosticable {
|
||||
dividerTheme: dividerTheme,
|
||||
buttonBarTheme: buttonBarTheme,
|
||||
bottomNavigationBarTheme: bottomNavigationBarTheme,
|
||||
timePickerTheme: timePickerTheme,
|
||||
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel,
|
||||
);
|
||||
}
|
||||
@ -527,6 +531,7 @@ class ThemeData with Diagnosticable {
|
||||
@required this.dividerTheme,
|
||||
@required this.buttonBarTheme,
|
||||
@required this.bottomNavigationBarTheme,
|
||||
@required this.timePickerTheme,
|
||||
@required this.fixTextFieldOutlineLabel,
|
||||
}) : assert(visualDensity != null),
|
||||
assert(primaryColor != null),
|
||||
@ -589,6 +594,7 @@ class ThemeData with Diagnosticable {
|
||||
assert(dividerTheme != null),
|
||||
assert(buttonBarTheme != null),
|
||||
assert(bottomNavigationBarTheme != null),
|
||||
assert(timePickerTheme != null),
|
||||
assert(fixTextFieldOutlineLabel != null);
|
||||
|
||||
/// Create a [ThemeData] based on the colors in the given [colorScheme] and
|
||||
@ -1036,6 +1042,9 @@ class ThemeData with Diagnosticable {
|
||||
/// widgets.
|
||||
final BottomNavigationBarThemeData bottomNavigationBarTheme;
|
||||
|
||||
/// A theme for customizing the appearance and layout of time picker widgets.
|
||||
final TimePickerThemeData timePickerTheme;
|
||||
|
||||
/// A temporary flag to allow apps to opt-in to a
|
||||
/// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y
|
||||
/// coordinate of the floating label in a [TextField] [OutlineInputBorder].
|
||||
@ -1117,6 +1126,7 @@ class ThemeData with Diagnosticable {
|
||||
DividerThemeData dividerTheme,
|
||||
ButtonBarThemeData buttonBarTheme,
|
||||
BottomNavigationBarThemeData bottomNavigationBarTheme,
|
||||
TimePickerThemeData timePickerTheme,
|
||||
bool fixTextFieldOutlineLabel,
|
||||
}) {
|
||||
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
|
||||
@ -1185,6 +1195,7 @@ class ThemeData with Diagnosticable {
|
||||
dividerTheme: dividerTheme ?? this.dividerTheme,
|
||||
buttonBarTheme: buttonBarTheme ?? this.buttonBarTheme,
|
||||
bottomNavigationBarTheme: bottomNavigationBarTheme ?? this.bottomNavigationBarTheme,
|
||||
timePickerTheme: timePickerTheme ?? this.timePickerTheme,
|
||||
fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel,
|
||||
);
|
||||
}
|
||||
@ -1331,6 +1342,7 @@ class ThemeData with Diagnosticable {
|
||||
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
||||
buttonBarTheme: ButtonBarThemeData.lerp(a.buttonBarTheme, b.buttonBarTheme, t),
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData.lerp(a.bottomNavigationBarTheme, b.bottomNavigationBarTheme, t),
|
||||
timePickerTheme: TimePickerThemeData.lerp(a.timePickerTheme, b.timePickerTheme, t),
|
||||
fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel,
|
||||
);
|
||||
}
|
||||
@ -1405,6 +1417,7 @@ class ThemeData with Diagnosticable {
|
||||
&& other.dividerTheme == dividerTheme
|
||||
&& other.buttonBarTheme == buttonBarTheme
|
||||
&& other.bottomNavigationBarTheme == bottomNavigationBarTheme
|
||||
&& other.timePickerTheme == timePickerTheme
|
||||
&& other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel;
|
||||
}
|
||||
|
||||
@ -1478,6 +1491,7 @@ class ThemeData with Diagnosticable {
|
||||
dividerTheme,
|
||||
buttonBarTheme,
|
||||
bottomNavigationBarTheme,
|
||||
timePickerTheme,
|
||||
fixTextFieldOutlineLabel,
|
||||
];
|
||||
return hashList(values);
|
||||
@ -1547,6 +1561,7 @@ class ThemeData with Diagnosticable {
|
||||
properties.add(DiagnosticsProperty<MaterialBannerThemeData>('bannerTheme', bannerTheme, defaultValue: defaultData.bannerTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<DividerThemeData>('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<ButtonBarThemeData>('buttonBarTheme', buttonBarTheme, defaultValue: defaultData.buttonBarTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<TimePickerThemeData>('timePickerTheme', timePickerTheme, defaultValue: defaultData.timePickerTheme, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<BottomNavigationBarThemeData>('bottomNavigationBarTheme', bottomNavigationBarTheme, defaultValue: defaultData.bottomNavigationBarTheme, level: DiagnosticLevel.debug));
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
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),
|
||||
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.start),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed),
|
||||
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.black),
|
||||
fixTextFieldOutlineLabel: false,
|
||||
);
|
||||
|
||||
@ -363,6 +364,7 @@ void main() {
|
||||
dividerTheme: const DividerThemeData(color: Colors.white),
|
||||
buttonBarTheme: const ButtonBarThemeData(alignment: MainAxisAlignment.end),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.shifting),
|
||||
timePickerTheme: const TimePickerThemeData(backgroundColor: Colors.white),
|
||||
fixTextFieldOutlineLabel: true,
|
||||
);
|
||||
|
||||
@ -430,6 +432,7 @@ void main() {
|
||||
dividerTheme: otherTheme.dividerTheme,
|
||||
buttonBarTheme: otherTheme.buttonBarTheme,
|
||||
bottomNavigationBarTheme: otherTheme.bottomNavigationBarTheme,
|
||||
timePickerTheme: otherTheme.timePickerTheme,
|
||||
fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel,
|
||||
);
|
||||
|
||||
@ -499,6 +502,7 @@ void main() {
|
||||
expect(themeDataCopy.dividerTheme, equals(otherTheme.dividerTheme));
|
||||
expect(themeDataCopy.buttonBarTheme, equals(otherTheme.buttonBarTheme));
|
||||
expect(themeDataCopy.bottomNavigationBarTheme, equals(otherTheme.bottomNavigationBarTheme));
|
||||
expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
|
||||
expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel));
|
||||
});
|
||||
|
||||
|
@ -6,15 +6,12 @@
|
||||
|
||||
@TestOn('!chrome') // entire file needs triage.
|
||||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../rendering/mock_canvas.dart';
|
||||
import '../rendering/recording_canvas.dart';
|
||||
import '../widgets/semantics_tester.dart';
|
||||
import 'feedback_tester.dart';
|
||||
|
||||
@ -23,10 +20,16 @@ final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widge
|
||||
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog');
|
||||
|
||||
class _TimePickerLauncher extends StatelessWidget {
|
||||
const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key);
|
||||
const _TimePickerLauncher({
|
||||
Key key,
|
||||
this.onChanged,
|
||||
this.locale,
|
||||
this.entryMode = TimePickerEntryMode.dial,
|
||||
}) : super(key: key);
|
||||
|
||||
final ValueChanged<TimeOfDay> onChanged;
|
||||
final Locale locale;
|
||||
final TimePickerEntryMode entryMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -35,17 +38,18 @@ class _TimePickerLauncher extends StatelessWidget {
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return RaisedButton(
|
||||
child: const Text('X'),
|
||||
onPressed: () async {
|
||||
onChanged(await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
builder: (BuildContext context) {
|
||||
return RaisedButton(
|
||||
child: const Text('X'),
|
||||
onPressed: () async {
|
||||
onChanged(await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||
initialEntryMode: entryMode,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -53,11 +57,15 @@ class _TimePickerLauncher extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged) async {
|
||||
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
|
||||
Future<Offset> startPicker(
|
||||
WidgetTester tester,
|
||||
ValueChanged<TimeOfDay> onChanged, {
|
||||
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
|
||||
}) async {
|
||||
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US'), entryMode: entryMode));
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
return tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||
return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
|
||||
}
|
||||
|
||||
Future<void> finishPicker(WidgetTester tester) async {
|
||||
@ -67,9 +75,13 @@ Future<void> finishPicker(WidgetTester tester) async {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Time picker', () {
|
||||
group('Time picker - Dial', () {
|
||||
_tests();
|
||||
});
|
||||
|
||||
group('Time picker - Input', () {
|
||||
_testsInput();
|
||||
});
|
||||
}
|
||||
|
||||
void _tests() {
|
||||
@ -170,6 +182,34 @@ void _tests() {
|
||||
expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
|
||||
});
|
||||
|
||||
testWidgets('tap-select rounds down to nearest 5 minute increment', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
|
||||
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
|
||||
final Offset min46 = Offset(center.dx - 50.0, center.dy - 5); // 46 mins
|
||||
|
||||
await tester.tapAt(hour6);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(min46);
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
|
||||
});
|
||||
|
||||
testWidgets('tap-select rounds up to nearest 5 minute increment', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
|
||||
final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
|
||||
final Offset min48 = Offset(center.dx - 50.0, center.dy - 15); // 48 mins
|
||||
|
||||
await tester.tapAt(hour6);
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
await tester.tapAt(min48);
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 6, minute: 50)));
|
||||
});
|
||||
|
||||
group('haptic feedback', () {
|
||||
const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
|
||||
const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
|
||||
@ -256,64 +296,18 @@ void _tests() {
|
||||
});
|
||||
|
||||
const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
|
||||
const List<String> labels12To11TwoDigit = <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
||||
const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
||||
|
||||
Future<void> mediaQueryBoilerplate(
|
||||
WidgetTester tester,
|
||||
bool alwaysUse24HourFormat, {
|
||||
TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
|
||||
double textScaleFactor = 1.0,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
Localizations(
|
||||
locale: const Locale('en', 'US'),
|
||||
delegates: const <LocalizationsDelegate<dynamic>>[
|
||||
DefaultMaterialLocalizations.delegate,
|
||||
DefaultWidgetsLocalizations.delegate,
|
||||
],
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(
|
||||
alwaysUse24HourFormat: alwaysUse24HourFormat,
|
||||
textScaleFactor: textScaleFactor,
|
||||
),
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Navigator(
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
return MaterialPageRoute<void>(builder: (BuildContext context) {
|
||||
return FlatButton(
|
||||
onPressed: () {
|
||||
showTimePicker(context: context, initialTime: initialTime);
|
||||
},
|
||||
child: const Text('X'),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
const List<String> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
|
||||
|
||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(tester, false);
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||
final dynamic dialPainter = dialPaint.painter;
|
||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
||||
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
|
||||
expect(dialPainter.primaryInnerLabels, null);
|
||||
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
|
||||
|
||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
||||
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
|
||||
expect(dialPainter.secondaryInnerLabels, null);
|
||||
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
|
||||
});
|
||||
|
||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
||||
@ -321,15 +315,11 @@ void _tests() {
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(findDialPaint);
|
||||
final dynamic dialPainter = dialPaint.painter;
|
||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
||||
expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
|
||||
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
|
||||
expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
|
||||
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||
expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
|
||||
|
||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
||||
expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
|
||||
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
|
||||
expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
|
||||
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||
expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
|
||||
});
|
||||
|
||||
testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
|
||||
@ -347,10 +337,10 @@ void _tests() {
|
||||
await mediaQueryBoilerplate(tester, true);
|
||||
|
||||
expect(semantics, isNot(includesNodeWith(label: ':')));
|
||||
expect(semantics.nodesWith(value: '00'), hasLength(2),
|
||||
reason: '00 appears once in the header, then again in the dial');
|
||||
expect(semantics.nodesWith(value: '07'), hasLength(2),
|
||||
reason: '07 appears once in the header, then again in the dial');
|
||||
expect(semantics.nodesWith(value: '00'), hasLength(1),
|
||||
reason: '00 appears once in the header');
|
||||
expect(semantics.nodesWith(value: '07'), hasLength(1),
|
||||
reason: '07 appears once in the header');
|
||||
expect(semantics, includesNodeWith(label: 'CANCEL'));
|
||||
expect(semantics, includesNodeWith(label: 'OK'));
|
||||
|
||||
@ -361,82 +351,6 @@ void _tests() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('provides semantics information for hours', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await mediaQueryBoilerplate(tester, true);
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||
final CustomPainter dialPainter = dialPaint.painter;
|
||||
final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
|
||||
|
||||
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
|
||||
painterTester.addLabel('13', 129.0, 11.5, 177.0, 59.5);
|
||||
painterTester.addLabel('14', 160.5, 43.0, 208.5, 91.0);
|
||||
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
|
||||
painterTester.addLabel('16', 160.5, 129.0, 208.5, 177.0);
|
||||
painterTester.addLabel('17', 129.0, 160.5, 177.0, 208.5);
|
||||
painterTester.addLabel('18', 86.0, 172.0, 134.0, 220.0);
|
||||
painterTester.addLabel('19', 43.0, 160.5, 91.0, 208.5);
|
||||
painterTester.addLabel('20', 11.5, 129.0, 59.5, 177.0);
|
||||
painterTester.addLabel('21', 0.0, 86.0, 48.0, 134.0);
|
||||
painterTester.addLabel('22', 11.5, 43.0, 59.5, 91.0);
|
||||
painterTester.addLabel('23', 43.0, 11.5, 91.0, 59.5);
|
||||
painterTester.addLabel('12', 86.0, 36.0, 134.0, 84.0);
|
||||
painterTester.addLabel('01', 111.0, 42.7, 159.0, 90.7);
|
||||
painterTester.addLabel('02', 129.3, 61.0, 177.3, 109.0);
|
||||
painterTester.addLabel('03', 136.0, 86.0, 184.0, 134.0);
|
||||
painterTester.addLabel('04', 129.3, 111.0, 177.3, 159.0);
|
||||
painterTester.addLabel('05', 111.0, 129.3, 159.0, 177.3);
|
||||
painterTester.addLabel('06', 86.0, 136.0, 134.0, 184.0);
|
||||
painterTester.addLabel('07', 61.0, 129.3, 109.0, 177.3);
|
||||
painterTester.addLabel('08', 42.7, 111.0, 90.7, 159.0);
|
||||
painterTester.addLabel('09', 36.0, 86.0, 84.0, 134.0);
|
||||
painterTester.addLabel('10', 42.7, 61.0, 90.7, 109.0);
|
||||
painterTester.addLabel('11', 61.0, 42.7, 109.0, 90.7);
|
||||
|
||||
painterTester.assertExpectations();
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('provides semantics information for minutes', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
await mediaQueryBoilerplate(tester, true);
|
||||
await tester.tap(_minuteControl);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||
final CustomPainter dialPainter = dialPaint.painter;
|
||||
final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
|
||||
|
||||
painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
|
||||
painterTester.addLabel('05', 129.0, 11.5, 177.0, 59.5);
|
||||
painterTester.addLabel('10', 160.5, 43.0, 208.5, 91.0);
|
||||
painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
|
||||
painterTester.addLabel('20', 160.5, 129.0, 208.5, 177.0);
|
||||
painterTester.addLabel('25', 129.0, 160.5, 177.0, 208.5);
|
||||
painterTester.addLabel('30', 86.0, 172.0, 134.0, 220.0);
|
||||
painterTester.addLabel('35', 43.0, 160.5, 91.0, 208.5);
|
||||
painterTester.addLabel('40', 11.5, 129.0, 59.5, 177.0);
|
||||
painterTester.addLabel('45', 0.0, 86.0, 48.0, 134.0);
|
||||
painterTester.addLabel('50', 11.5, 43.0, 59.5, 91.0);
|
||||
painterTester.addLabel('55', 43.0, 11.5, 91.0, 59.5);
|
||||
|
||||
painterTester.assertExpectations();
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('picks the right dial ring from widget configuration', (WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 12, minute: 0));
|
||||
dynamic dialPaint = tester.widget(findDialPaint);
|
||||
expect('${dialPaint.painter.activeRing}', '_DialRing.inner');
|
||||
|
||||
await tester.pumpWidget(Container()); // make sure previous state isn't reused
|
||||
|
||||
await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 0, minute: 0));
|
||||
dialPaint = tester.widget(findDialPaint);
|
||||
expect('${dialPaint.painter.activeRing}', '_DialRing.outer');
|
||||
});
|
||||
|
||||
testWidgets('can increment and decrement hours', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
@ -550,21 +464,15 @@ void _tests() {
|
||||
});
|
||||
|
||||
testWidgets('header touch regions are large enough', (WidgetTester tester) async {
|
||||
// Ensure picker is displayed in portrait mode.
|
||||
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
|
||||
tester.binding.window.devicePixelRatioTestValue = 1;
|
||||
await mediaQueryBoilerplate(tester, false);
|
||||
|
||||
final Size amSize = tester.getSize(find.ancestor(
|
||||
of: find.text('AM'),
|
||||
matching: find.byType(InkWell),
|
||||
));
|
||||
expect(amSize.width, greaterThanOrEqualTo(48.0));
|
||||
expect(amSize.height, greaterThanOrEqualTo(48.0));
|
||||
|
||||
final Size pmSize = tester.getSize(find.ancestor(
|
||||
of: find.text('PM'),
|
||||
matching: find.byType(InkWell),
|
||||
));
|
||||
expect(pmSize.width, greaterThanOrEqualTo(48.0));
|
||||
expect(pmSize.height, greaterThanOrEqualTo(48.0));
|
||||
final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
|
||||
expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
|
||||
// Height should be double the minimum size to account for both AM/PM stacked.
|
||||
expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));
|
||||
|
||||
final Size hourSize = tester.getSize(find.ancestor(
|
||||
of: find.text('7'),
|
||||
@ -579,6 +487,9 @@ void _tests() {
|
||||
));
|
||||
expect(minuteSize.width, greaterThanOrEqualTo(48.0));
|
||||
expect(minuteSize.height, greaterThanOrEqualTo(48.0));
|
||||
|
||||
tester.binding.window.physicalSizeTestValue = null;
|
||||
tester.binding.window.devicePixelRatioTestValue = null;
|
||||
});
|
||||
|
||||
testWidgets('builder parameter', (WidgetTester tester) async {
|
||||
@ -696,51 +607,158 @@ void _tests() {
|
||||
expect(nestedObserver.pickerCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('optional text parameters are utilized', (WidgetTester tester) async {
|
||||
const String cancelText = 'Custom Cancel';
|
||||
const String confirmText = 'Custom OK';
|
||||
const String helperText = 'Custom Help';
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return RaisedButton(
|
||||
child: const Text('X'),
|
||||
onPressed: () async {
|
||||
await showTimePicker(
|
||||
context: context,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 0),
|
||||
cancelText: cancelText,
|
||||
confirmText: confirmText,
|
||||
helpText: helperText,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
));
|
||||
|
||||
// Open the picker.
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text(cancelText), findsOneWidget);
|
||||
expect(find.text(confirmText), findsOneWidget);
|
||||
expect(find.text(helperText), findsOneWidget);
|
||||
});
|
||||
|
||||
// TODO(rami-a): Re-enable and fix test.
|
||||
testWidgets('text scale affects certain elements and not others',
|
||||
(WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 1.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
(WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 1.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
|
||||
final double amHeight = tester.getSize(find.text('AM')).height;
|
||||
|
||||
await tester.tap(find.text('OK')); // dismiss the dialog
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that the time display is not affected by text scale.
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 2.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final double amHeight2x = tester.getSize(find.text('AM')).height;
|
||||
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
|
||||
expect(amHeight2x, greaterThanOrEqualTo(amHeight * 2));
|
||||
|
||||
await tester.tap(find.text('OK')); // dismiss the dialog
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify that text scale for AM/PM is at most 2x.
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 3.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
|
||||
expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
|
||||
});
|
||||
}
|
||||
|
||||
void _testsInput() {
|
||||
testWidgets('Initial entry mode is used', (WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Initial time is the default', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 7, minute: 0)));
|
||||
});
|
||||
|
||||
testWidgets('Help text is used - Input', (WidgetTester tester) async {
|
||||
const String helpText = 'help';
|
||||
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, helpText: helpText);
|
||||
expect(find.text(helpText), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
|
||||
await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
|
||||
await tester.tap(find.byIcon(Icons.access_time));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
});
|
||||
|
||||
final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
|
||||
final double amHeight = tester.getSize(find.text('AM')).height;
|
||||
|
||||
await tester.tap(find.text('OK')); // dismiss the dialog
|
||||
await tester.pumpAndSettle();
|
||||
testWidgets('Entered text returns time', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||
await tester.enterText(find.byType(TextField).first, '9');
|
||||
await tester.enterText(find.byType(TextField).last, '12');
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
|
||||
});
|
||||
|
||||
// Verify that the time display is not affected by text scale.
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 2.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||
await tester.enterText(find.byType(TextField).first, '8');
|
||||
await tester.enterText(find.byType(TextField).last, '15');
|
||||
await tester.tap(find.byIcon(Icons.access_time));
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
|
||||
});
|
||||
|
||||
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
|
||||
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
|
||||
testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
|
||||
TimeOfDay result;
|
||||
await startPicker(tester, (TimeOfDay time) { result = time; }, entryMode: TimePickerEntryMode.input);
|
||||
|
||||
await tester.tap(find.text('OK')); // dismiss the dialog
|
||||
await tester.pumpAndSettle();
|
||||
// Invalid hour.
|
||||
await tester.enterText(find.byType(TextField).first, '88');
|
||||
await tester.enterText(find.byType(TextField).last, '15');
|
||||
await finishPicker(tester);
|
||||
expect(result, null);
|
||||
|
||||
// Verify that text scale for AM/PM is at most 2x.
|
||||
await mediaQueryBoilerplate(
|
||||
tester,
|
||||
false,
|
||||
textScaleFactor: 3.0,
|
||||
initialTime: const TimeOfDay(hour: 7, minute: 41),
|
||||
);
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
// Invalid minute.
|
||||
await tester.enterText(find.byType(TextField).first, '8');
|
||||
await tester.enterText(find.byType(TextField).last, '150');
|
||||
await finishPicker(tester);
|
||||
expect(result, null);
|
||||
|
||||
expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
|
||||
expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
|
||||
await tester.enterText(find.byType(TextField).first, '8');
|
||||
await tester.enterText(find.byType(TextField).last, '15');
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
|
||||
});
|
||||
}
|
||||
|
||||
@ -749,73 +767,6 @@ final Finder findDialPaint = find.descendant(
|
||||
matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
|
||||
);
|
||||
|
||||
class _SemanticsNodeExpectation {
|
||||
_SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);
|
||||
|
||||
final String label;
|
||||
final double left;
|
||||
final double top;
|
||||
final double right;
|
||||
final double bottom;
|
||||
}
|
||||
|
||||
class _CustomPainterSemanticsTester {
|
||||
_CustomPainterSemanticsTester(this.tester, this.painter, this.semantics);
|
||||
|
||||
final WidgetTester tester;
|
||||
final CustomPainter painter;
|
||||
final SemanticsTester semantics;
|
||||
final PaintPattern expectedLabels = paints;
|
||||
final List<_SemanticsNodeExpectation> expectedNodes = <_SemanticsNodeExpectation>[];
|
||||
|
||||
void addLabel(String label, double left, double top, double right, double bottom) {
|
||||
expectedNodes.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
|
||||
}
|
||||
|
||||
void assertExpectations() {
|
||||
final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
|
||||
painter.paint(canvasRecording, const Size(220.0, 220.0));
|
||||
final List<ui.Paragraph> paragraphs = canvasRecording.invocations
|
||||
.where((RecordedInvocation recordedInvocation) {
|
||||
return recordedInvocation.invocation.memberName == #drawParagraph;
|
||||
})
|
||||
.map<ui.Paragraph>((RecordedInvocation recordedInvocation) {
|
||||
return recordedInvocation.invocation.positionalArguments.first as ui.Paragraph;
|
||||
})
|
||||
.toList();
|
||||
|
||||
final PaintPattern expectedLabels = paints;
|
||||
int i = 0;
|
||||
|
||||
for (final _SemanticsNodeExpectation expectation in expectedNodes) {
|
||||
expect(semantics, includesNodeWith(value: expectation.label));
|
||||
final Iterable<SemanticsNode> dialLabelNodes = semantics
|
||||
.nodesWith(value: expectation.label)
|
||||
.where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
|
||||
expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
|
||||
final Rect rect = Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
|
||||
expect(dialLabelNodes.single.rect, within(distance: 1.0, from: rect),
|
||||
reason: 'This is checking the node rectangle for label ${expectation.label}');
|
||||
|
||||
final ui.Paragraph paragraph = paragraphs[i++];
|
||||
|
||||
// The label text paragraph and the semantics node share the same center,
|
||||
// but have different sizes.
|
||||
final Offset center = dialLabelNodes.single.rect.center;
|
||||
final Offset topLeft = center.translate(
|
||||
-paragraph.width / 2.0,
|
||||
-paragraph.height / 2.0,
|
||||
);
|
||||
|
||||
expectedLabels.paragraph(
|
||||
paragraph: paragraph,
|
||||
offset: within<Offset>(distance: 1.0, from: topLeft),
|
||||
);
|
||||
}
|
||||
expect(tester.renderObject(findDialPaint), expectedLabels);
|
||||
}
|
||||
}
|
||||
|
||||
class PickerObserver extends NavigatorObserver {
|
||||
int pickerCount = 0;
|
||||
|
||||
@ -827,3 +778,53 @@ class PickerObserver extends NavigatorObserver {
|
||||
super.didPush(route, previousRoute);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mediaQueryBoilerplate(
|
||||
WidgetTester tester,
|
||||
bool alwaysUse24HourFormat, {
|
||||
TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
|
||||
double textScaleFactor = 1.0,
|
||||
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
|
||||
String helpText,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
Localizations(
|
||||
locale: const Locale('en', 'US'),
|
||||
delegates: const <LocalizationsDelegate<dynamic>>[
|
||||
DefaultMaterialLocalizations.delegate,
|
||||
DefaultWidgetsLocalizations.delegate,
|
||||
],
|
||||
child: MediaQuery(
|
||||
data: MediaQueryData(
|
||||
alwaysUse24HourFormat: alwaysUse24HourFormat,
|
||||
textScaleFactor: textScaleFactor,
|
||||
),
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Navigator(
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
return MaterialPageRoute<void>(builder: (BuildContext context) {
|
||||
return FlatButton(
|
||||
onPressed: () {
|
||||
showTimePicker(
|
||||
context: context,
|
||||
initialTime: initialTime,
|
||||
initialEntryMode: entryMode,
|
||||
helpText: helpText,
|
||||
);
|
||||
},
|
||||
child: const Text('X'),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
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(
|
||||
WidgetTester tester,
|
||||
ValueChanged<TimeOfDay> onChanged, {
|
||||
Locale locale = const Locale('en', 'US'),
|
||||
Locale locale = const Locale('en', 'US'),
|
||||
}) async {
|
||||
await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: locale,));
|
||||
await tester.tap(find.text('X'));
|
||||
@ -58,66 +58,151 @@ Future<void> finishPicker(WidgetTester tester) async {
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('can localize the header in all known formats', (WidgetTester tester) async {
|
||||
// TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them
|
||||
final Map<Locale, List<String>> locales = <Locale, List<String>>{
|
||||
const Locale('en', 'US'): const <String>['hour', 'string :', 'minute', 'period'], //'h:mm a'
|
||||
const Locale('en', 'GB'): const <String>['hour', 'string :', 'minute'], //'HH:mm'
|
||||
const Locale('es', 'ES'): const <String>['hour', 'string :', 'minute'], //'H:mm'
|
||||
const Locale('fr', 'CA'): const <String>['hour', 'string h', 'minute'], //'HH \'h\' mm'
|
||||
const Locale('zh', 'ZH'): const <String>['period', 'hour', 'string :', 'minute'], //'ah:mm'
|
||||
};
|
||||
testWidgets('can localize the header in all known formats - portrait', (WidgetTester tester) async {
|
||||
// Ensure picker is displayed in portrait mode.
|
||||
tester.binding.window.physicalSizeTestValue = const Size(400, 800);
|
||||
tester.binding.window.devicePixelRatioTestValue = 1;
|
||||
|
||||
for (final Locale locale in locales.keys) {
|
||||
final Finder stringFragmentTextFinder = find.descendant(
|
||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
|
||||
matching: find.byType(Text),
|
||||
).first;
|
||||
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
|
||||
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteControl');
|
||||
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
|
||||
|
||||
// TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them
|
||||
final List<Locale> locales = <Locale>[
|
||||
const Locale('en', 'US'), //'h:mm a'
|
||||
const Locale('en', 'GB'), //'HH:mm'
|
||||
const Locale('es', 'ES'), //'H:mm'
|
||||
const Locale('fr', 'CA'), //'HH \'h\' mm'
|
||||
const Locale('zh', 'ZH'), //'ah:mm'
|
||||
];
|
||||
|
||||
for (final Locale locale in locales) {
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale);
|
||||
final List<String> actual = <String>[];
|
||||
tester.element(find.byType(CustomMultiChildLayout)).visitChildren((Element child) {
|
||||
final LayoutId layout = child.widget as LayoutId;
|
||||
final String fragmentType = '${layout.child.runtimeType}';
|
||||
final dynamic widget = layout.child;
|
||||
if (fragmentType == '_MinuteControl') {
|
||||
actual.add('minute');
|
||||
} else if (fragmentType == '_DayPeriodControl') {
|
||||
actual.add('period');
|
||||
} else if (fragmentType == '_HourControl') {
|
||||
actual.add('hour');
|
||||
} else if (fragmentType == '_StringFragment') {
|
||||
actual.add('string ${widget.value}');
|
||||
} else {
|
||||
fail('Unsupported fragment type: $fragmentType');
|
||||
}
|
||||
});
|
||||
expect(actual, locales[locale]);
|
||||
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
|
||||
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
|
||||
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
|
||||
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
|
||||
|
||||
if (locale == const Locale('en', 'US')) {
|
||||
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(minuteLeftOffset, lessThan(dayPeriodLeftOffset));
|
||||
} else if (locale == const Locale('en', 'GB')) {
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('es', 'ES')) {
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('fr', 'CA')) {
|
||||
expect(stringFragmentText.data, 'h');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('zh', 'ZH')) {
|
||||
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(dayPeriodLeftOffset, lessThan(hourLeftOffset));
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
}
|
||||
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
|
||||
await finishPicker(tester);
|
||||
}
|
||||
|
||||
tester.binding.window.physicalSizeTestValue = null;
|
||||
tester.binding.window.devicePixelRatioTestValue = null;
|
||||
});
|
||||
|
||||
testWidgets('uses single-ring 12-hour dial for h hour format', (WidgetTester tester) async {
|
||||
// Tap along the segment stretching from the center to the edge at
|
||||
// 12:00 AM position. Because there's only one ring, no matter where you
|
||||
// tap the time will be the same. See the 24-hour dial test that behaves
|
||||
// differently.
|
||||
for (int i = 1; i < 10; i++) {
|
||||
TimeOfDay result;
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
|
||||
final Size size = tester.getSize(find.byKey(const Key('time-picker-dial')));
|
||||
final double dy = (size.height / 2.0 / 10) * i;
|
||||
await tester.tapAt(Offset(center.dx, center.dy - dy));
|
||||
testWidgets('can localize the header in all known formats - landscape', (WidgetTester tester) async {
|
||||
// Ensure picker is displayed in landscape mode.
|
||||
tester.binding.window.physicalSizeTestValue = const Size(800, 400);
|
||||
tester.binding.window.devicePixelRatioTestValue = 1;
|
||||
|
||||
final Finder stringFragmentTextFinder = find.descendant(
|
||||
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment'),
|
||||
matching: find.byType(Text),
|
||||
).first;
|
||||
final Finder hourControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
|
||||
final Finder minuteControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteControl');
|
||||
final Finder dayPeriodControlFinder = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl');
|
||||
|
||||
// TODO(yjbanov): also test `HH.mm` (in_ID), `a h:mm` (ko_KR) and `HH:mm น.` (th_TH) when we have .arb files for them
|
||||
final List<Locale> locales = <Locale>[
|
||||
const Locale('en', 'US'), //'h:mm a'
|
||||
const Locale('en', 'GB'), //'HH:mm'
|
||||
const Locale('es', 'ES'), //'H:mm'
|
||||
const Locale('fr', 'CA'), //'HH \'h\' mm'
|
||||
const Locale('zh', 'ZH'), //'ah:mm'
|
||||
];
|
||||
|
||||
for (final Locale locale in locales) {
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { }, locale: locale);
|
||||
final Text stringFragmentText = tester.widget(stringFragmentTextFinder);
|
||||
final double hourLeftOffset = tester.getTopLeft(hourControlFinder).dx;
|
||||
final double hourTopOffset = tester.getTopLeft(hourControlFinder).dy;
|
||||
final double minuteLeftOffset = tester.getTopLeft(minuteControlFinder).dx;
|
||||
final double stringFragmentLeftOffset = tester.getTopLeft(stringFragmentTextFinder).dx;
|
||||
|
||||
if (locale == const Locale('en', 'US')) {
|
||||
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||
final double dayPeriodTopOffset = tester.getTopLeft(dayPeriodControlFinder).dy;
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(hourLeftOffset, dayPeriodLeftOffset);
|
||||
expect(hourTopOffset, lessThan(dayPeriodTopOffset));
|
||||
} else if (locale == const Locale('en', 'GB')) {
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('es', 'ES')) {
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('fr', 'CA')) {
|
||||
expect(stringFragmentText.data, 'h');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(dayPeriodControlFinder, findsNothing);
|
||||
} else if (locale == const Locale('zh', 'ZH')) {
|
||||
final double dayPeriodLeftOffset = tester.getTopLeft(dayPeriodControlFinder).dx;
|
||||
final double dayPeriodTopOffset = tester.getTopLeft(dayPeriodControlFinder).dy;
|
||||
expect(stringFragmentText.data, ':');
|
||||
expect(hourLeftOffset, lessThan(stringFragmentLeftOffset));
|
||||
expect(stringFragmentLeftOffset, lessThan(minuteLeftOffset));
|
||||
expect(hourLeftOffset, dayPeriodLeftOffset);
|
||||
expect(hourTopOffset, greaterThan(dayPeriodTopOffset));
|
||||
}
|
||||
await tester.tapAt(Offset(center.dx, center.dy - 50.0));
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
|
||||
}
|
||||
|
||||
tester.binding.window.physicalSizeTestValue = null;
|
||||
tester.binding.window.devicePixelRatioTestValue = null;
|
||||
});
|
||||
|
||||
testWidgets('uses two-ring 24-hour dial for H and HH hour formats', (WidgetTester tester) async {
|
||||
testWidgets('uses single-ring 24-hour dial for all formats', (WidgetTester tester) async {
|
||||
const List<Locale> locales = <Locale>[
|
||||
Locale('en', 'US'), // h
|
||||
Locale('en', 'GB'), // HH
|
||||
Locale('es', 'ES'), // H
|
||||
];
|
||||
for (final Locale locale in locales) {
|
||||
// Tap along the segment stretching from the center to the edge at
|
||||
// 12:00 AM position. There are two rings. At ~70% mark, the ring
|
||||
// switches between inner ring and outer ring.
|
||||
// 12:00 AM position. Because there's only one ring, no matter where you
|
||||
// tap the time will be the same.
|
||||
for (int i = 1; i < 10; i++) {
|
||||
TimeOfDay result;
|
||||
final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; }, locale: locale);
|
||||
@ -125,14 +210,13 @@ void main() {
|
||||
final double dy = (size.height / 2.0 / 10) * i;
|
||||
await tester.tapAt(Offset(center.dx, center.dy - dy));
|
||||
await finishPicker(tester);
|
||||
expect(result, equals(TimeOfDay(hour: i < 7 ? 12 : 0, minute: 0)));
|
||||
expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
|
||||
const List<String> labels12To11TwoDigit = <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
|
||||
const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
|
||||
const List<String> labels00To22TwoDigit = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
|
||||
|
||||
Future<void> mediaQueryBoilerplate(WidgetTester tester, bool alwaysUse24HourFormat) async {
|
||||
await tester.pumpWidget(
|
||||
@ -174,19 +258,17 @@ void main() {
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||
final dynamic dialPainter = dialPaint.painter;
|
||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
||||
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||
expect(
|
||||
primaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
primaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels12To11,
|
||||
);
|
||||
expect(dialPainter.primaryInnerLabels, null);
|
||||
|
||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
||||
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||
expect(
|
||||
secondaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels12To11,
|
||||
);
|
||||
expect(dialPainter.secondaryInnerLabels, null);
|
||||
});
|
||||
|
||||
testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
|
||||
@ -194,26 +276,16 @@ void main() {
|
||||
|
||||
final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
|
||||
final dynamic dialPainter = dialPaint.painter;
|
||||
final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
|
||||
final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
|
||||
expect(
|
||||
primaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels00To23,
|
||||
);
|
||||
final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
|
||||
expect(
|
||||
primaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels12To11TwoDigit,
|
||||
primaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels00To22TwoDigit,
|
||||
);
|
||||
|
||||
final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
|
||||
final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
|
||||
expect(
|
||||
secondaryOuterLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels00To23,
|
||||
);
|
||||
final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
|
||||
expect(
|
||||
secondaryInnerLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels12To11TwoDigit,
|
||||
secondaryLabels.map<String>((dynamic tp) => ((tp.painter as TextPainter).text as TextSpan).text),
|
||||
labels00To22TwoDigit,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user