Migrated Switch to Material 3 (#110095)

This commit is contained in:
Qun Cheng 2022-09-09 11:53:48 -07:00 committed by GitHub
parent e3f8ee8806
commit 75fac6ae4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 2285 additions and 338 deletions

View File

@ -31,6 +31,7 @@ import 'package:gen_defaults/input_decorator_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/surface_tint.dart';
import 'package:gen_defaults/switch_template.dart';
import 'package:gen_defaults/text_field_template.dart';
import 'package:gen_defaults/typography_template.dart';
@ -121,6 +122,7 @@ Future<void> main(List<String> args) async {
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile();
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
}

View File

@ -0,0 +1,208 @@
// 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 'template.dart';
class SwitchTemplate extends TokenTemplate {
const SwitchTemplate(super.blockName, super.fileName, super.tokens, {
super.colorSchemePrefix = '_colors.',
});
@override
String generate() => '''
class _${blockName}DefaultsM3 extends SwitchThemeData {
_${blockName}DefaultsM3(BuildContext context)
: _colors = Theme.of(context).colorScheme;
final ColorScheme _colors;
@override
MaterialStateProperty<Color> get thumbColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.switch.disabled.selected.handle')};
}
return ${componentColor('md.comp.switch.disabled.unselected.handle')};
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.selected.pressed.handle')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.selected.hover.handle')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.selected.focus.handle')};
}
return ${componentColor('md.comp.switch.selected.handle')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.unselected.pressed.handle')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.unselected.hover.handle')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.unselected.focus.handle')};
}
return ${componentColor('md.comp.switch.unselected.handle')};
});
}
@override
MaterialStateProperty<Color> get trackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.switch.disabled.selected.track')}.withOpacity(${opacity('md.comp.switch.disabled.track.opacity')});
}
return ${componentColor('md.comp.switch.disabled.unselected.track')}.withOpacity(${opacity('md.comp.switch.disabled.track.opacity')});
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.selected.pressed.track')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.selected.hover.track')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.selected.focus.track')};
}
return ${componentColor('md.comp.switch.selected.track')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.unselected.pressed.track')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.unselected.hover.track')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.unselected.focus.track')};
}
return ${componentColor('md.comp.switch.unselected.track')};
});
}
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.selected.pressed.state-layer')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.selected.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.selected.focus.state-layer')};
}
return null;
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.unselected.pressed.state-layer')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.unselected.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.unselected.focus.state-layer')};
}
return null;
});
}
@override
double get splashRadius => ${tokens['md.comp.switch.state-layer.size']} / 2;
}
class _SwitchConfigM3 with _SwitchConfig {
_SwitchConfigM3(this.context)
: _colors = Theme.of(context).colorScheme;
BuildContext context;
final ColorScheme _colors;
static const double iconSize = ${tokens['md.comp.switch.unselected.icon.size']};
@override
double get activeThumbRadius => ${tokens['md.comp.switch.selected.handle.width']} / 2;
@override
MaterialStateProperty<Color> get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.switch.disabled.selected.icon')};
}
return ${componentColor('md.comp.switch.disabled.unselected.icon')};
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.selected.pressed.icon')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.selected.hover.icon')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.selected.focus.icon')};
}
return ${componentColor('md.comp.switch.selected.icon')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.switch.unselected.pressed.icon')};
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.switch.unselected.hover.icon')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.switch.unselected.focus.icon')};
}
return ${componentColor('md.comp.switch.unselected.icon')};
});
}
@override
double get inactiveThumbRadius => ${tokens['md.comp.switch.unselected.handle.width']} / 2;
@override
double get pressedThumbRadius => ${tokens['md.comp.switch.pressed.handle.width']} / 2;
@override
double get switchHeight => _kSwitchMinSize + 8.0;
@override
double get switchHeightCollapsed => _kSwitchMinSize;
@override
double get switchWidth => trackWidth - 2 * (trackHeight / 2.0) + _kSwitchMinSize;
@override
double get thumbRadiusWithIcon => ${tokens['md.comp.switch.with-icon.handle.width']} / 2;
@override
List<BoxShadow>? get thumbShadow => kElevationToShadow[0];
@override
double get trackHeight => ${tokens['md.comp.switch.track.height']};
@override
MaterialStateProperty<Color?> get trackOutlineColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return null;
}
if (states.contains(MaterialState.disabled)) {
return ${componentColor('md.comp.switch.disabled.unselected.track.outline')}.withOpacity(${opacity('md.comp.switch.disabled.track.opacity')});
}
return ${componentColor('md.comp.switch.unselected.track.outline')};
});
}
@override
double get trackWidth => ${tokens['md.comp.switch.track.width']};
}
''';
}

View File

@ -0,0 +1,74 @@
// 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.
// Flutter code sample for Switch
import 'package:flutter/material.dart';
void main() => runApp(const SwitchApp());
class SwitchApp extends StatelessWidget {
const SwitchApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4)),
home: Scaffold(
appBar: AppBar(title: const Text('Switch Sample')),
body: const Center(
child: SwitchExample(),
),
),
);
}
}
class SwitchExample extends StatefulWidget {
const SwitchExample({super.key});
@override
State<SwitchExample> createState() => _SwitchExampleState();
}
class _SwitchExampleState extends State<SwitchExample> {
bool light0 = true;
bool light1 = true;
bool light2 = true;
final MaterialStateProperty<Icon?> thumbIcon = MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
// Thumb icon when the switch is selected.
if (states.contains(MaterialState.selected)) {
return const Icon(Icons.check);
}
return const Icon(Icons.close);
},
);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Switch(
value: light0,
onChanged: (bool value) {
setState(() {
light0 = value;
});
},
),
Switch(
thumbIcon: thumbIcon,
value: light1,
onChanged: (bool value) {
setState(() {
light1 = value;
});
},
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,7 @@ class SwitchThemeData with Diagnosticable {
this.mouseCursor,
this.overlayColor,
this.splashRadius,
this.thumbIcon,
});
/// {@macro flutter.material.switch.thumbColor}
@ -76,6 +77,11 @@ class SwitchThemeData with Diagnosticable {
/// If specified, overrides the default value of [Switch.splashRadius].
final double? splashRadius;
/// {@macro flutter.material.switch.thumbIcon}
///
/// It is overridden by [Switch.thumbIcon].
final MaterialStateProperty<Icon?>? thumbIcon;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SwitchThemeData copyWith({
@ -85,6 +91,7 @@ class SwitchThemeData with Diagnosticable {
MaterialStateProperty<MouseCursor?>? mouseCursor,
MaterialStateProperty<Color?>? overlayColor,
double? splashRadius,
MaterialStateProperty<Icon?>? thumbIcon,
}) {
return SwitchThemeData(
thumbColor: thumbColor ?? this.thumbColor,
@ -93,6 +100,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor: mouseCursor ?? this.mouseCursor,
overlayColor: overlayColor ?? this.overlayColor,
splashRadius: splashRadius ?? this.splashRadius,
thumbIcon: thumbIcon ?? this.thumbIcon,
);
}
@ -107,6 +115,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
overlayColor: MaterialStateProperty.lerp<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t),
thumbIcon: t < 0.5 ? a?.thumbIcon : b?.thumbIcon,
);
}
@ -118,6 +127,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor,
overlayColor,
splashRadius,
thumbIcon,
);
@override
@ -134,7 +144,8 @@ class SwitchThemeData with Diagnosticable {
&& other.materialTapTargetSize == materialTapTargetSize
&& other.mouseCursor == mouseCursor
&& other.overlayColor == overlayColor
&& other.splashRadius == splashRadius;
&& other.splashRadius == splashRadius
&& other.thumbIcon == thumbIcon;
}
@override
@ -146,6 +157,7 @@ class SwitchThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
properties.add(DoubleProperty('splashRadius', splashRadius, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Icon?>>('thumbIcon', thumbIcon, defaultValue: null));
}
}

View File

@ -1252,7 +1252,7 @@ class ThemeData with Diagnosticable {
/// * Typography: `typography` (see table above)
///
/// ### Components
/// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton]
/// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton], [IconButton]
/// * FAB: [FloatingActionButton]
/// * Extended FAB: [FloatingActionButton.extended]
/// * Cards: [Card]
@ -1266,6 +1266,7 @@ class ThemeData with Diagnosticable {
/// * Lists: [ListTile]
/// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar])
/// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail]
/// * Switch: [Switch]
/// * Top app bar: [AppBar]
///
/// In addition, this flag enables features introduced in Android 12.

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ void main() {
expect(themeData.materialTapTargetSize, null);
expect(themeData.overlayColor, null);
expect(themeData.splashRadius, null);
expect(themeData.thumbIcon, null);
const SwitchTheme theme = SwitchTheme(data: SwitchThemeData(), child: SizedBox());
expect(theme.data.thumbColor, null);
@ -31,6 +32,7 @@ void main() {
expect(theme.data.materialTapTargetSize, null);
expect(theme.data.overlayColor, null);
expect(theme.data.splashRadius, null);
expect(theme.data.thumbIcon, null);
});
testWidgets('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async {
@ -54,6 +56,7 @@ void main() {
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
overlayColor: MaterialStatePropertyAll<Color>(Color(0xfffffff2)),
splashRadius: 1.0,
thumbIcon: MaterialStatePropertyAll<Icon>(Icon(IconData(123))),
).debugFillProperties(builder);
final List<String> description = builder.properties
@ -67,6 +70,7 @@ void main() {
expect(description[3], 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))');
expect(description[4], 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff2))');
expect(description[5], 'splashRadius: 1.0');
expect(description[6], 'thumbIcon: MaterialStatePropertyAll(Icon(IconData(U+0007B)))');
});
testWidgets('Switch is themeable', (WidgetTester tester) async {
@ -81,10 +85,11 @@ void main() {
const Color focusOverlayColor = Color(0xfffffff4);
const Color hoverOverlayColor = Color(0xfffffff5);
const double splashRadius = 1.0;
const Icon icon1 = Icon(Icons.check);
const Icon icon2 = Icon(Icons.close);
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: ThemeData(
final ThemeData themeData = ThemeData(
useMaterial3: true,
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
@ -110,8 +115,18 @@ void main() {
return null;
}),
splashRadius: splashRadius,
thumbIcon: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return icon1;
}
return icon2;
}),
),
),
);
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Switch(
dragStartBehavior: DragStartBehavior.down,
@ -128,27 +143,39 @@ void main() {
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor)
..paragraph()
)
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor),
..circle(color: defaultThumbColor)
)
);
// Size from MaterialTapTargetSize.shrinkWrap.
expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0));
expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0));
// Selected switch.
await tester.pumpWidget(buildSwitch(selected: true));
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor)..paragraph())
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor),
..circle(color: selectedThumbColor))
);
// Switch with hover.
@ -187,9 +214,7 @@ void main() {
const Color hoverColor = Color(0xffffff5f);
const double splashRadius = 2.0;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: ThemeData(
final ThemeData themeData = ThemeData(
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
@ -215,8 +240,19 @@ void main() {
return null;
}),
splashRadius: themeSplashRadius,
thumbIcon: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return null;
}
return null;
}),
),
),
);
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Switch(
value: selected,
@ -239,6 +275,12 @@ void main() {
focusColor: focusColor,
hoverColor: hoverColor,
splashRadius: splashRadius,
thumbIcon: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const Icon(Icons.add);
}
return const Icon(Icons.access_alarm);
}),
),
),
);
@ -249,27 +291,36 @@ void main() {
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor)..paragraph(offset: const Offset(12, 16)))
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor),
..circle(color: defaultThumbColor))
);
// Size from MaterialTapTargetSize.shrinkWrap.
expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0));
expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0));
// Selected switch.
await tester.pumpWidget(buildSwitch(selected: true));
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor),
..circle(color: selectedThumbColor))
);
// Switch with hover.
@ -298,9 +349,7 @@ void main() {
const Color defaultTrackColor = Color(0xffffff2f);
const Color selectedTrackColor = Color(0xffffff3f);
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: ThemeData(
final ThemeData themeData = ThemeData(
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
@ -315,7 +364,12 @@ void main() {
return themeDefaultTrackColor;
}),
),
),
);
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Switch(
value: selected,
@ -335,12 +389,17 @@ void main() {
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor))
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor),
..circle(color: defaultThumbColor))
);
// Selected switch.
@ -348,12 +407,16 @@ void main() {
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor),
..circle(color: selectedThumbColor))
);
});
@ -371,15 +434,17 @@ void main() {
return null;
}
const double splashRadius = 24.0;
Widget buildSwitch({required bool active}) {
return MaterialApp(
theme: ThemeData(
final ThemeData themeData = ThemeData(
switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.resolveWith(getOverlayColor),
splashRadius: splashRadius,
),
),
);
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({required bool active}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: Switch(
value: active,
@ -395,12 +460,20 @@ void main() {
expect(
_getSwitchMaterial(tester),
paints
material3
? ((paints
..rrect()
..rrect())
..circle(
color: inactivePressedOverlayColor,
radius: splashRadius,
))
: (paints
..rrect()
..circle(
color: inactivePressedOverlayColor,
radius: splashRadius,
),
)),
reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor',
);
@ -426,14 +499,16 @@ void main() {
const Color localThemeThumbColor = Color(0xffff0000);
const Color localThemeTrackColor = Color(0xffff0000);
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: ThemeData(
final ThemeData themeData = ThemeData(
switchTheme: const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(globalThemeThumbColor),
trackColor: MaterialStatePropertyAll<Color>(globalThemeTrackColor),
),
),
);
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: SwitchTheme(
data: const SwitchThemeData(
@ -454,12 +529,16 @@ void main() {
await tester.pumpAndSettle();
expect(
_getSwitchMaterial(tester),
paints
material3
? (paints
..rrect(color: localThemeTrackColor)
..circle(color: localThemeThumbColor))
: (paints
..rrect(color: localThemeTrackColor)
..circle()
..circle()
..circle()
..circle(color: localThemeThumbColor),
..circle(color: localThemeThumbColor))
);
});
}