Skip to content

Commit fae458b

Browse files
authored
Convert TimePicker to Material 3 (#116396)
* Make some minor changes in preparation for updating the Time Picker to M3 * Revert OutlineInputBorder.borderRadius type change * Revert more OutlineInputBorder.borderRadius changes. * Convert TimePicker to Material 3 * Add example test * Revert OutlineInputBorder.borderRadius type change * Fix test * Review Changes * Merge changes * Some sizing and elevation fixes * Fix localization tests
1 parent a59dd83 commit fae458b

File tree

9 files changed

+4940
-2601
lines changed

9 files changed

+4940
-2601
lines changed

dev/tools/gen_defaults/bin/gen_defaults.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import 'package:gen_defaults/surface_tint.dart';
4949
import 'package:gen_defaults/switch_template.dart';
5050
import 'package:gen_defaults/tabs_template.dart';
5151
import 'package:gen_defaults/text_field_template.dart';
52+
import 'package:gen_defaults/time_picker_template.dart';
5253
import 'package:gen_defaults/typography_template.dart';
5354

5455
Map<String, dynamic> _readTokenFile(String fileName) {
@@ -167,6 +168,7 @@ Future<void> main(List<String> args) async {
167168
SliderTemplate('md.comp.slider', 'Slider', '$materialLib/slider.dart', tokens).updateFile();
168169
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
169170
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
171+
TimePickerTemplate('TimePicker', '$materialLib/time_picker.dart', tokens).updateFile();
170172
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
171173
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
172174
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'template.dart';
6+
7+
class TimePickerTemplate extends TokenTemplate {
8+
const TimePickerTemplate(super.blockName, super.fileName, super.tokens, {
9+
super.colorSchemePrefix = '_colors.',
10+
super.textThemePrefix = '_textTheme.'
11+
});
12+
13+
static const String tokenGroup = 'md.comp.time-picker';
14+
static const String hourMinuteComponent = '$tokenGroup.time-selector';
15+
static const String dayPeriodComponent = '$tokenGroup.period-selector';
16+
static const String dialComponent = '$tokenGroup.clock-dial';
17+
static const String variant = '';
18+
19+
@override
20+
String generate() => '''
21+
// Generated version ${tokens["version"]}
22+
class _${blockName}DefaultsM3 extends _TimePickerDefaults {
23+
_${blockName}DefaultsM3(this.context);
24+
25+
final BuildContext context;
26+
27+
late final ColorScheme _colors = Theme.of(context).colorScheme;
28+
late final TextTheme _textTheme = Theme.of(context).textTheme;
29+
30+
@override
31+
Color get backgroundColor {
32+
return ${componentColor("$tokenGroup.container")};
33+
}
34+
35+
@override
36+
ButtonStyle get cancelButtonStyle {
37+
return TextButton.styleFrom();
38+
}
39+
40+
@override
41+
ButtonStyle get confirmButtonStyle {
42+
return TextButton.styleFrom();
43+
}
44+
45+
@override
46+
BorderSide get dayPeriodBorderSide {
47+
return ${border('$dayPeriodComponent.outline')};
48+
}
49+
50+
@override
51+
Color get dayPeriodColor {
52+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
53+
if (states.contains(MaterialState.selected)) {
54+
return ${componentColor("$dayPeriodComponent.selected.container")};
55+
}
56+
// The unselected day period should match the overall picker dialog color.
57+
// Making it transparent enables that without being redundant and allows
58+
// the optional elevation overlay for dark mode to be visible.
59+
return Colors.transparent;
60+
});
61+
}
62+
63+
@override
64+
OutlinedBorder get dayPeriodShape {
65+
return ${shape("$dayPeriodComponent.container")}.copyWith(side: dayPeriodBorderSide);
66+
}
67+
68+
@override
69+
Size get dayPeriodPortraitSize {
70+
return ${size('$dayPeriodComponent.vertical.container')};
71+
}
72+
73+
@override
74+
Size get dayPeriodLandscapeSize {
75+
return ${size('$dayPeriodComponent.horizontal.container')};
76+
}
77+
78+
@override
79+
Size get dayPeriodInputSize {
80+
// Input size is eight pixels smaller than the portrait size in the spec,
81+
// but there's not token for it yet.
82+
return Size(dayPeriodPortraitSize.width, dayPeriodPortraitSize.height - 8);
83+
}
84+
85+
@override
86+
Color get dayPeriodTextColor {
87+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
88+
return _dayPeriodForegroundColor.resolve(states);
89+
});
90+
}
91+
92+
MaterialStateProperty<Color> get _dayPeriodForegroundColor {
93+
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
94+
Color? textColor;
95+
if (states.contains(MaterialState.selected)) {
96+
if (states.contains(MaterialState.pressed)) {
97+
textColor = ${componentColor("$dayPeriodComponent.selected.pressed.label-text")};
98+
} else {
99+
// not pressed
100+
if (states.contains(MaterialState.focused)) {
101+
textColor = ${componentColor("$dayPeriodComponent.selected.focus.label-text")};
102+
} else {
103+
// not focused
104+
if (states.contains(MaterialState.hovered)) {
105+
textColor = ${componentColor("$dayPeriodComponent.selected.hover.label-text")};
106+
}
107+
}
108+
}
109+
} else {
110+
// unselected
111+
if (states.contains(MaterialState.pressed)) {
112+
textColor = ${componentColor("$dayPeriodComponent.unselected.pressed.label-text")};
113+
} else {
114+
// not pressed
115+
if (states.contains(MaterialState.focused)) {
116+
textColor = ${componentColor("$dayPeriodComponent.unselected.focus.label-text")};
117+
} else {
118+
// not focused
119+
if (states.contains(MaterialState.hovered)) {
120+
textColor = ${componentColor("$dayPeriodComponent.unselected.hover.label-text")};
121+
}
122+
}
123+
}
124+
}
125+
return textColor ?? ${componentColor("$dayPeriodComponent.selected.label-text")};
126+
});
127+
}
128+
129+
@override
130+
TextStyle get dayPeriodTextStyle {
131+
return ${textStyle("$dayPeriodComponent.label-text")}!.copyWith(color: dayPeriodTextColor);
132+
}
133+
134+
@override
135+
Color get dialBackgroundColor {
136+
return ${componentColor(dialComponent)}.withOpacity(_colors.brightness == Brightness.dark ? 0.12 : 0.08);
137+
}
138+
139+
@override
140+
Color get dialHandColor {
141+
return ${componentColor('$dialComponent.selector.handle.container')};
142+
}
143+
144+
@override
145+
Size get dialSize {
146+
return ${size("$dialComponent.container")};
147+
}
148+
149+
@override
150+
double get handWidth {
151+
return ${size("$dialComponent.selector.track.container")}.width;
152+
}
153+
154+
@override
155+
double get dotRadius {
156+
return ${size("$dialComponent.selector.handle.container")}.width / 2;
157+
}
158+
159+
@override
160+
double get centerRadius {
161+
return ${size("$dialComponent.selector.center.container")}.width / 2;
162+
}
163+
164+
@override
165+
Color get dialTextColor {
166+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
167+
if (states.contains(MaterialState.selected)) {
168+
return ${componentColor('$dialComponent.selected.label-text')};
169+
}
170+
return ${componentColor('$dialComponent.unselected.label-text')};
171+
});
172+
}
173+
174+
@override
175+
TextStyle get dialTextStyle {
176+
return ${textStyle('$dialComponent.label-text')}!;
177+
}
178+
179+
@override
180+
double get elevation {
181+
return ${elevation("$tokenGroup.container")};
182+
}
183+
184+
@override
185+
Color get entryModeIconColor {
186+
return _colors.onSurface;
187+
}
188+
189+
@override
190+
TextStyle get helpTextStyle {
191+
return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
192+
final TextStyle textStyle = ${textStyle('$tokenGroup.headline')}!;
193+
return textStyle.copyWith(color: ${componentColor('$tokenGroup.headline')});
194+
});
195+
}
196+
197+
@override
198+
EdgeInsetsGeometry get padding {
199+
return const EdgeInsets.all(24);
200+
}
201+
202+
@override
203+
Color get hourMinuteColor {
204+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
205+
if (states.contains(MaterialState.selected)) {
206+
Color overlayColor = ${componentColor('$hourMinuteComponent.selected.container')};
207+
if (states.contains(MaterialState.pressed)) {
208+
overlayColor = ${componentColor('$hourMinuteComponent.selected.pressed.state-layer')};
209+
} else if (states.contains(MaterialState.focused)) {
210+
const double focusOpacity = ${opacity('$hourMinuteComponent.focus.state-layer.opacity')};
211+
overlayColor = ${componentColor('$hourMinuteComponent.selected.focus.state-layer')}.withOpacity(focusOpacity);
212+
} else if (states.contains(MaterialState.hovered)) {
213+
const double hoverOpacity = ${opacity('$hourMinuteComponent.hover.state-layer.opacity')};
214+
overlayColor = ${componentColor('$hourMinuteComponent.selected.hover.state-layer')}.withOpacity(hoverOpacity);
215+
}
216+
return Color.alphaBlend(overlayColor, ${componentColor('$hourMinuteComponent.selected.container')});
217+
} else {
218+
Color overlayColor = ${componentColor('$hourMinuteComponent.unselected.container')};
219+
if (states.contains(MaterialState.pressed)) {
220+
overlayColor = ${componentColor('$hourMinuteComponent.unselected.pressed.state-layer')};
221+
} else if (states.contains(MaterialState.focused)) {
222+
const double focusOpacity = ${opacity('$hourMinuteComponent.focus.state-layer.opacity')};
223+
overlayColor = ${componentColor('$hourMinuteComponent.unselected.focus.state-layer')}.withOpacity(focusOpacity);
224+
} else if (states.contains(MaterialState.hovered)) {
225+
const double hoverOpacity = ${opacity('$hourMinuteComponent.hover.state-layer.opacity')};
226+
overlayColor = ${componentColor('$hourMinuteComponent.unselected.hover.state-layer')}.withOpacity(hoverOpacity);
227+
}
228+
return Color.alphaBlend(overlayColor, ${componentColor('$hourMinuteComponent.unselected.container')});
229+
}
230+
});
231+
}
232+
233+
@override
234+
ShapeBorder get hourMinuteShape {
235+
return ${shape('$hourMinuteComponent.container')};
236+
}
237+
238+
@override
239+
Size get hourMinuteSize {
240+
return ${size('$hourMinuteComponent.container')};
241+
}
242+
243+
@override
244+
Size get hourMinuteSize24Hour {
245+
return Size(${size('$hourMinuteComponent.24h-vertical.container')}.width, hourMinuteSize.height);
246+
}
247+
248+
@override
249+
Size get hourMinuteInputSize {
250+
// Input size is eight pixels smaller than the regular size in the spec, but
251+
// there's not token for it yet.
252+
return Size(hourMinuteSize.width, hourMinuteSize.height - 8);
253+
}
254+
255+
@override
256+
Size get hourMinuteInputSize24Hour {
257+
// Input size is eight pixels smaller than the regular size in the spec, but
258+
// there's not token for it yet.
259+
return Size(hourMinuteSize24Hour.width, hourMinuteSize24Hour.height - 8);
260+
}
261+
262+
@override
263+
Color get hourMinuteTextColor {
264+
return MaterialStateColor.resolveWith((Set<MaterialState> states) {
265+
return _hourMinuteTextColor.resolve(states);
266+
});
267+
}
268+
269+
MaterialStateProperty<Color> get _hourMinuteTextColor {
270+
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
271+
if (states.contains(MaterialState.selected)) {
272+
if (states.contains(MaterialState.pressed)) {
273+
return ${componentColor("$hourMinuteComponent.selected.pressed.label-text")};
274+
}
275+
if (states.contains(MaterialState.focused)) {
276+
return ${componentColor("$hourMinuteComponent.selected.focus.label-text")};
277+
}
278+
if (states.contains(MaterialState.hovered)) {
279+
return ${componentColor("$hourMinuteComponent.selected.hover.label-text")};
280+
}
281+
return ${componentColor("$hourMinuteComponent.selected.label-text")};
282+
} else {
283+
// unselected
284+
if (states.contains(MaterialState.pressed)) {
285+
return ${componentColor("$hourMinuteComponent.unselected.pressed.label-text")};
286+
}
287+
if (states.contains(MaterialState.focused)) {
288+
return ${componentColor("$hourMinuteComponent.unselected.focus.label-text")};
289+
}
290+
if (states.contains(MaterialState.hovered)) {
291+
return ${componentColor("$hourMinuteComponent.unselected.hover.label-text")};
292+
}
293+
return ${componentColor("$hourMinuteComponent.unselected.label-text")};
294+
}
295+
});
296+
}
297+
298+
@override
299+
TextStyle get hourMinuteTextStyle {
300+
return MaterialStateTextStyle.resolveWith((Set<MaterialState> states) {
301+
return ${textStyle('$hourMinuteComponent.label-text')}!.copyWith(color: _hourMinuteTextColor.resolve(states));
302+
});
303+
}
304+
305+
@override
306+
InputDecorationTheme get inputDecorationTheme {
307+
// This is NOT correct, but there's no token for
308+
// 'time-input.container.shape', so this is using the radius from the shape
309+
// for the hour/minute selector.
310+
final BorderRadiusGeometry selectorRadius = ${shape('$hourMinuteComponent.container')}.borderRadius;
311+
return InputDecorationTheme(
312+
contentPadding: EdgeInsets.zero,
313+
filled: true,
314+
// This should be derived from a token, but there isn't one for 'time-input'.
315+
fillColor: hourMinuteColor,
316+
// This should be derived from a token, but there isn't one for 'time-input'.
317+
focusColor: _colors.primaryContainer,
318+
enabledBorder: OutlineInputBorder(
319+
borderRadius: selectorRadius,
320+
borderSide: const BorderSide(color: Colors.transparent),
321+
),
322+
errorBorder: OutlineInputBorder(
323+
borderRadius: selectorRadius,
324+
borderSide: BorderSide(color: _colors.error, width: 2),
325+
),
326+
focusedBorder: OutlineInputBorder(
327+
borderRadius: selectorRadius,
328+
borderSide: BorderSide(color: _colors.primary, width: 2),
329+
),
330+
focusedErrorBorder: OutlineInputBorder(
331+
borderRadius: selectorRadius,
332+
borderSide: BorderSide(color: _colors.error, width: 2),
333+
),
334+
hintStyle: hourMinuteTextStyle.copyWith(color: _colors.onSurface.withOpacity(0.36)),
335+
// Prevent the error text from appearing.
336+
// TODO(rami-a): Remove this workaround once
337+
// https://github.com/flutter/flutter/issues/54104
338+
// is fixed.
339+
errorStyle: const TextStyle(fontSize: 0, height: 0),
340+
);
341+
}
342+
343+
@override
344+
ShapeBorder get shape {
345+
return ${shape("$tokenGroup.container")};
346+
}
347+
}
348+
''';
349+
}

0 commit comments

Comments
 (0)