Skip to content

Commit 95cdebe

Browse files
authored
Add timeSelectorSeparatorColor and timeSelectorSeparatorTextStyle for Material 3 Time Picker (#143739)
fixes [`Time selector separator` in TimePicker is not centered vertically](flutter/flutter#143691) Separator currently `hourMinuteTextStyle` to style itself. This introduces `timeSelectorSeparatorColor` and `timeSelectorSeparatorTextStyle` from Material 3 specs to correctly style the separator. This also adds ability to change separator color without changing `hourMinuteTextColor`. ### Specs for the time selector separator https://m3.material.io/components/time-pickers/specs ![image](https://github.com/flutter/flutter/assets/48603081/0c84f649-545d-441b-adbf-2b9ec872b14c) ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @OverRide Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( // timePickerTheme: TimePickerThemeData( // hourMinuteTextColor: Colors.amber, // ) ), home: Scaffold( body: Center( child: Builder(builder: (context) { return ElevatedButton( onPressed: () async { await showTimePicker( context: context, initialTime: TimeOfDay.now(), ); }, child: const Text('Pick Time'), ); }), ), ), ); } } ``` </details> | Before | After | | --------------- | --------------- | | <img src="https://github.com/flutter/flutter/assets/48603081/20beeba4-5cc2-49ee-bba8-1c552c0d1e44" /> | <img src="https://github.com/flutter/flutter/assets/48603081/24927187-aff7-4191-930c-bceab6a4b4c2" /> |
1 parent f923375 commit 95cdebe

File tree

6 files changed

+170
-38
lines changed

6 files changed

+170
-38
lines changed

dev/tools/gen_defaults/lib/time_picker_template.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,20 @@ class _${blockName}DefaultsM3 extends _TimePickerDefaults {
333333
ShapeBorder get shape {
334334
return ${shape("$tokenGroup.container")};
335335
}
336+
337+
@override
338+
MaterialStateProperty<Color?>? get timeSelectorSeparatorColor {
339+
// TODO(tahatesser): Update this when tokens are available.
340+
// This is taken from https://m3.material.io/components/time-pickers/specs.
341+
return MaterialStatePropertyAll<Color>(_colors.onSurface);
342+
}
343+
344+
@override
345+
MaterialStateProperty<TextStyle?>? get timeSelectorSeparatorTextStyle {
346+
// TODO(tahatesser): Update this when tokens are available.
347+
// This is taken from https://m3.material.io/components/time-pickers/specs.
348+
return MaterialStatePropertyAll<TextStyle?>(_textTheme.displayLarge);
349+
}
336350
}
337351
''';
338352
}

packages/flutter/lib/src/material/time_picker.dart

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ class _TimePickerHeader extends StatelessWidget {
242242
textDirection: TextDirection.ltr,
243243
children: <Widget>[
244244
const Expanded(child: _HourControl()),
245-
_StringFragment(timeOfDayFormat: timeOfDayFormat),
245+
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
246246
const Expanded(child: _MinuteControl()),
247247
],
248248
),
@@ -278,7 +278,7 @@ class _TimePickerHeader extends StatelessWidget {
278278
textDirection: TextDirection.ltr,
279279
children: <Widget>[
280280
const Expanded(child: _HourControl()),
281-
_StringFragment(timeOfDayFormat: timeOfDayFormat),
281+
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
282282
const Expanded(child: _MinuteControl()),
283283
],
284284
),
@@ -428,12 +428,12 @@ class _HourControl extends StatelessWidget {
428428
/// A passive fragment showing a string value.
429429
///
430430
/// Used to display the appropriate separator between the input fields.
431-
class _StringFragment extends StatelessWidget {
432-
const _StringFragment({ required this.timeOfDayFormat });
431+
class _TimeSelectorSeparator extends StatelessWidget {
432+
const _TimeSelectorSeparator({ required this.timeOfDayFormat });
433433

434434
final TimeOfDayFormat timeOfDayFormat;
435435

436-
String _stringFragmentValue(TimeOfDayFormat timeOfDayFormat) {
436+
String _timeSelectorSeparatorValue(TimeOfDayFormat timeOfDayFormat) {
437437
switch (timeOfDayFormat) {
438438
case TimeOfDayFormat.h_colon_mm_space_a:
439439
case TimeOfDayFormat.a_space_h_colon_mm:
@@ -455,11 +455,17 @@ class _StringFragment extends StatelessWidget {
455455
final Set<MaterialState> states = <MaterialState>{};
456456

457457
final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(
458-
timePickerTheme.hourMinuteTextColor ?? defaultTheme.hourMinuteTextColor,
458+
timePickerTheme.timeSelectorSeparatorColor?.resolve(states)
459+
?? timePickerTheme.hourMinuteTextColor
460+
?? defaultTheme.timeSelectorSeparatorColor?.resolve(states)
461+
?? defaultTheme.hourMinuteTextColor,
459462
states,
460463
);
461464
final TextStyle effectiveStyle = MaterialStateProperty.resolveAs<TextStyle>(
462-
timePickerTheme.hourMinuteTextStyle ?? defaultTheme.hourMinuteTextStyle,
465+
timePickerTheme.timeSelectorSeparatorTextStyle?.resolve(states)
466+
?? timePickerTheme.hourMinuteTextStyle
467+
?? defaultTheme.timeSelectorSeparatorTextStyle?.resolve(states)
468+
?? defaultTheme.hourMinuteTextStyle,
463469
states,
464470
).copyWith(color: effectiveTextColor);
465471

@@ -478,7 +484,7 @@ class _StringFragment extends StatelessWidget {
478484
width: timeOfDayFormat == TimeOfDayFormat.frenchCanadian ? 36 : 24,
479485
height: height,
480486
child: Text(
481-
_stringFragmentValue(timeOfDayFormat),
487+
_timeSelectorSeparatorValue(timeOfDayFormat),
482488
style: effectiveStyle,
483489
textScaler: TextScaler.noScaling,
484490
textAlign: TextAlign.center,
@@ -1801,7 +1807,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi
18011807
],
18021808
),
18031809
),
1804-
_StringFragment(timeOfDayFormat: timeOfDayFormat),
1810+
_TimeSelectorSeparator(timeOfDayFormat: timeOfDayFormat),
18051811
Expanded(
18061812
child: Column(
18071813
crossAxisAlignment: CrossAxisAlignment.start,
@@ -3655,6 +3661,20 @@ class _TimePickerDefaultsM3 extends _TimePickerDefaults {
36553661
ShapeBorder get shape {
36563662
return const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0)));
36573663
}
3664+
3665+
@override
3666+
MaterialStateProperty<Color?>? get timeSelectorSeparatorColor {
3667+
// TODO(tahatesser): Update this when tokens are available.
3668+
// This is taken from https://m3.material.io/components/time-pickers/specs.
3669+
return MaterialStatePropertyAll<Color>(_colors.onSurface);
3670+
}
3671+
3672+
@override
3673+
MaterialStateProperty<TextStyle?>? get timeSelectorSeparatorTextStyle {
3674+
// TODO(tahatesser): Update this when tokens are available.
3675+
// This is taken from https://m3.material.io/components/time-pickers/specs.
3676+
return MaterialStatePropertyAll<TextStyle?>(_textTheme.displayLarge);
3677+
}
36583678
}
36593679

36603680
// END GENERATED TOKEN PROPERTIES - TimePicker

packages/flutter/lib/src/material/time_picker_theme.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class TimePickerThemeData with Diagnosticable {
6262
this.inputDecorationTheme,
6363
this.padding,
6464
this.shape,
65+
this.timeSelectorSeparatorColor,
66+
this.timeSelectorSeparatorTextStyle,
6567
}) : _dayPeriodColor = dayPeriodColor;
6668

6769
/// The background color of a time picker.
@@ -261,6 +263,25 @@ class TimePickerThemeData with Diagnosticable {
261263
/// `RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))`.
262264
final ShapeBorder? shape;
263265

266+
/// The color of the time selector seperator between the hour and minute controls.
267+
///
268+
/// if this is null, the time picker defaults to the overall theme's
269+
/// [ColorScheme.onSurface].
270+
///
271+
/// If this is null and [ThemeData.useMaterial3] is false, then defaults to the value of
272+
/// [hourMinuteTextColor].
273+
final MaterialStateProperty<Color?>? timeSelectorSeparatorColor;
274+
275+
/// Used to configure the text style for the time selector seperator between the hour
276+
/// and minute controls.
277+
///
278+
/// If this is null, the time picker defaults to the overall theme's
279+
/// [TextTheme.displayLarge].
280+
///
281+
/// If this is null and [ThemeData.useMaterial3] is false, then defaults to the value of
282+
/// [hourMinuteTextStyle].
283+
final MaterialStateProperty<TextStyle?>? timeSelectorSeparatorTextStyle;
284+
264285
/// Creates a copy of this object with the given fields replaced with the
265286
/// new values.
266287
TimePickerThemeData copyWith({
@@ -287,6 +308,8 @@ class TimePickerThemeData with Diagnosticable {
287308
InputDecorationTheme? inputDecorationTheme,
288309
EdgeInsetsGeometry? padding,
289310
ShapeBorder? shape,
311+
MaterialStateProperty<Color?>? timeSelectorSeparatorColor,
312+
MaterialStateProperty<TextStyle?>? timeSelectorSeparatorTextStyle,
290313
}) {
291314
return TimePickerThemeData(
292315
backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -311,6 +334,8 @@ class TimePickerThemeData with Diagnosticable {
311334
inputDecorationTheme: inputDecorationTheme ?? this.inputDecorationTheme,
312335
padding: padding ?? this.padding,
313336
shape: shape ?? this.shape,
337+
timeSelectorSeparatorColor: timeSelectorSeparatorColor ?? this.timeSelectorSeparatorColor,
338+
timeSelectorSeparatorTextStyle: timeSelectorSeparatorTextStyle ?? this.timeSelectorSeparatorTextStyle,
314339
);
315340
}
316341

@@ -355,6 +380,8 @@ class TimePickerThemeData with Diagnosticable {
355380
inputDecorationTheme: t < 0.5 ? a?.inputDecorationTheme : b?.inputDecorationTheme,
356381
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
357382
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
383+
timeSelectorSeparatorColor: MaterialStateProperty.lerp<Color?>(a?.timeSelectorSeparatorColor, b?.timeSelectorSeparatorColor, t, Color.lerp),
384+
timeSelectorSeparatorTextStyle: MaterialStateProperty.lerp<TextStyle?>(a?.timeSelectorSeparatorTextStyle, b?.timeSelectorSeparatorTextStyle, t, TextStyle.lerp),
358385
);
359386
}
360387

@@ -382,6 +409,8 @@ class TimePickerThemeData with Diagnosticable {
382409
inputDecorationTheme,
383410
padding,
384411
shape,
412+
timeSelectorSeparatorColor,
413+
timeSelectorSeparatorTextStyle,
385414
]);
386415

387416
@override
@@ -414,7 +443,9 @@ class TimePickerThemeData with Diagnosticable {
414443
&& other.hourMinuteTextStyle == hourMinuteTextStyle
415444
&& other.inputDecorationTheme == inputDecorationTheme
416445
&& other.padding == padding
417-
&& other.shape == shape;
446+
&& other.shape == shape
447+
&& other.timeSelectorSeparatorColor == timeSelectorSeparatorColor
448+
&& other.timeSelectorSeparatorTextStyle == timeSelectorSeparatorTextStyle;
418449
}
419450

420451
@override
@@ -442,6 +473,8 @@ class TimePickerThemeData with Diagnosticable {
442473
properties.add(DiagnosticsProperty<InputDecorationTheme>('inputDecorationTheme', inputDecorationTheme, defaultValue: null));
443474
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
444475
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
476+
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('timeSelectorSeparatorColor', timeSelectorSeparatorColor, defaultValue: null));
477+
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('timeSelectorSeparatorTextStyle', timeSelectorSeparatorTextStyle, defaultValue: null));
445478
}
446479
}
447480

packages/flutter/test/material/time_picker_test.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,7 +1831,7 @@ void main() {
18311831
final double minuteFieldTop =
18321832
tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy;
18331833
final double separatorTop =
1834-
tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy;
1834+
tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TimeSelectorSeparator')).dy;
18351835
expect(hourFieldTop, separatorTop);
18361836
expect(minuteFieldTop, separatorTop);
18371837
});
@@ -1965,6 +1965,32 @@ void main() {
19651965
});
19661966
});
19671967
}
1968+
1969+
testWidgets('Material3 - Time selector separator default text style', (WidgetTester tester) async {
1970+
final ThemeData theme = ThemeData();
1971+
await startPicker(
1972+
tester,
1973+
(TimeOfDay? value) { },
1974+
theme: theme,
1975+
);
1976+
1977+
final RenderParagraph paragraph = tester.renderObject(find.text(':'));
1978+
expect(paragraph.text.style!.color, theme.colorScheme.onSurface);
1979+
expect(paragraph.text.style!.fontSize, 57.0);
1980+
});
1981+
1982+
testWidgets('Material2 - Time selector separator default text style', (WidgetTester tester) async {
1983+
final ThemeData theme = ThemeData(useMaterial3: false);
1984+
await startPicker(
1985+
tester,
1986+
(TimeOfDay? value) { },
1987+
theme: theme,
1988+
);
1989+
1990+
final RenderParagraph paragraph = tester.renderObject(find.text(':'));
1991+
expect(paragraph.text.style!.color, theme.colorScheme.onSurface);
1992+
expect(paragraph.text.style!.fontSize, 56.0);
1993+
});
19681994
}
19691995

19701996
final Finder findDialPaint = find.descendant(
@@ -2175,10 +2201,11 @@ Future<Offset?> startPicker(
21752201
ValueChanged<TimeOfDay?> onChanged, {
21762202
TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
21772203
String? restorationId,
2178-
required MaterialType materialType,
2204+
ThemeData? theme,
2205+
MaterialType? materialType,
21792206
}) async {
21802207
await tester.pumpWidget(MaterialApp(
2181-
theme: ThemeData(useMaterial3: materialType == MaterialType.material3),
2208+
theme: theme ?? ThemeData(useMaterial3: materialType == MaterialType.material3),
21822209
restorationScopeId: 'app',
21832210
locale: const Locale('en', 'US'),
21842211
home: _TimePickerLauncher(

packages/flutter/test/material/time_picker_theme_test.dart

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ void main() {
4242
expect(timePickerTheme.entryModeIconColor, null);
4343
expect(timePickerTheme.padding, null);
4444
expect(timePickerTheme.shape, null);
45+
expect(timePickerTheme.timeSelectorSeparatorColor, null);
46+
expect(timePickerTheme.timeSelectorSeparatorTextStyle, null);
4547
});
4648

4749
testWidgets('Default TimePickerThemeData debugFillProperties', (WidgetTester tester) async {
@@ -89,6 +91,8 @@ void main() {
8991
shape: RoundedRectangleBorder(
9092
side: BorderSide(color: Color(0xfffffff3)),
9193
),
94+
timeSelectorSeparatorColor: MaterialStatePropertyAll<Color>(Color(0xfffffff4)),
95+
timeSelectorSeparatorTextStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(color: Color(0xfffffff5))),
9296
).debugFillProperties(builder);
9397

9498
final List<String> description = builder.properties
@@ -118,7 +122,9 @@ void main() {
118122
'hourMinuteTextStyle: TextStyle(inherit: true, color: Color(0xfffffff1))',
119123
'inputDecorationTheme: InputDecorationTheme#ff861(labelStyle: TextStyle(inherit: true, color: Color(0xfffffff2)))',
120124
'padding: EdgeInsets.all(1.0)',
121-
'shape: RoundedRectangleBorder(BorderSide(color: Color(0xfffffff3)), BorderRadius.zero)'
125+
'shape: RoundedRectangleBorder(BorderSide(color: Color(0xfffffff3)), BorderRadius.zero)',
126+
'timeSelectorSeparatorColor: MaterialStatePropertyAll(Color(0xfffffff4))',
127+
'timeSelectorSeparatorTextStyle: MaterialStatePropertyAll(TextStyle(inherit: true, color: Color(0xfffffff5)))'
122128
]));
123129
});
124130

@@ -798,6 +804,38 @@ void main() {
798804
final Material pmMaterial = _textMaterial(tester, 'PM');
799805
expect(pmMaterial.color, Colors.blue);
800806
});
807+
808+
testWidgets('Time selector separator color uses the timeSelectorSeparatorColor value', (WidgetTester tester) async {
809+
final TimePickerThemeData timePickerTheme = _timePickerTheme().copyWith(
810+
timeSelectorSeparatorColor: const MaterialStatePropertyAll<Color>(Color(0xff00ff00))
811+
);
812+
final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme);
813+
await tester.pumpWidget(_TimePickerLauncher(themeData: theme, entryMode: TimePickerEntryMode.input));
814+
await tester.tap(find.text('X'));
815+
await tester.pumpAndSettle(const Duration(seconds: 1));
816+
817+
final RenderParagraph paragraph = tester.renderObject(find.text(':'));
818+
expect(paragraph.text.style!.color, const Color(0xff00ff00));
819+
});
820+
821+
testWidgets('Time selector separator text style uses the timeSelectorSeparatorTextStyle value', (WidgetTester tester) async {
822+
final TimePickerThemeData timePickerTheme = _timePickerTheme().copyWith(
823+
timeSelectorSeparatorTextStyle: const MaterialStatePropertyAll<TextStyle>(
824+
TextStyle(
825+
fontSize: 35.0,
826+
fontStyle: FontStyle.italic,
827+
),
828+
),
829+
);
830+
final ThemeData theme = ThemeData(timePickerTheme: timePickerTheme);
831+
await tester.pumpWidget(_TimePickerLauncher(themeData: theme, entryMode: TimePickerEntryMode.input));
832+
await tester.tap(find.text('X'));
833+
await tester.pumpAndSettle(const Duration(seconds: 1));
834+
835+
final RenderParagraph paragraph = tester.renderObject(find.text(':'));
836+
expect(paragraph.text.style!.fontSize, 35.0);
837+
expect(paragraph.text.style!.fontStyle, FontStyle.italic);
838+
});
801839
}
802840

803841
final Color _selectedColor = Colors.green[100]!;

0 commit comments

Comments
 (0)