Unverified Commit 75fac6ae authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Migrated `Switch` to Material 3 (#110095)

parent e3f8ee88
......@@ -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();
}
// 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']};
}
''';
}
// 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;
});
},
),
],
);
}
}
......@@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'color_scheme.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
......@@ -21,14 +24,7 @@ import 'toggleable.dart';
// bool _giveVerse = true;
// late StateSetter setState;
const double _kTrackHeight = 14.0;
const double _kTrackWidth = 33.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchMinSize = kMinInteractiveDimension - 8.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + _kSwitchMinSize;
const double _kSwitchHeight = _kSwitchMinSize + 8.0;
const double _kSwitchHeightCollapsed = _kSwitchMinSize;
enum _SwitchType { material, adaptive }
......@@ -48,6 +44,10 @@ enum _SwitchType { material, adaptive }
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// Material Design 3 provides the option to add icons on the thumb of the [Switch].
/// If [ThemeData.useMaterial3] is set to true, users can use [Switch.thumbIcon]
/// to add optional Icons based on the different [MaterialState]s of the [Switch].
///
/// {@tool dartpad}
/// This example shows a toggleable [Switch]. When the thumb slides to the other
/// side of the track, the switch is toggled between on/off.
......@@ -62,6 +62,13 @@ enum _SwitchType { material, adaptive }
/// ** See code in examples/api/lib/material/switch/switch.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to add icons on the thumb of the [Switch] using the
/// [Switch.thumbIcon] property.
///
/// ** See code in examples/api/lib/material/switch/switch.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [SwitchListTile], which combines this widget with a [ListTile] so that
......@@ -98,6 +105,7 @@ class Switch extends StatelessWidget {
this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.thumbIcon,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
......@@ -142,6 +150,7 @@ class Switch extends StatelessWidget {
this.materialTapTargetSize,
this.thumbColor,
this.trackColor,
this.thumbIcon,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
this.focusColor,
......@@ -322,6 +331,39 @@ class Switch extends StatelessWidget {
/// | Disabled | `Colors.black12` | `Colors.white10` |
final MaterialStateProperty<Color?>? trackColor;
/// {@template flutter.material.switch.thumbIcon}
/// The icon to use on the thumb of this switch
///
/// Resolved in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// {@tool snippet}
/// This example resolves the [thumbIcon] based on the current
/// [MaterialState] of the [Switch], providing a different [Icon] when it is
/// [MaterialState.disabled].
///
/// ```dart
/// Switch(
/// value: true,
/// onChanged: (_) => true,
/// thumbIcon: MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) {
/// return const Icon(Icons.close);
/// }
/// return null; // All other states will use the default thumbIcon.
/// }),
/// )
/// ```
/// {@end-tool}
/// {@endtemplate}
///
/// If null, then the value of [SwitchThemeData.thumbIcon] is used. If this is also null,
/// then the [Switch] does not have any icons on the thumb.
final MaterialStateProperty<Icon?>? thumbIcon;
/// {@template flutter.material.switch.materialTapTargetSize}
/// Configures the minimum size of the tap target.
/// {@endtemplate}
......@@ -419,15 +461,16 @@ class Switch extends StatelessWidget {
Size _getSwitchSize(BuildContext context) {
final ThemeData theme = Theme.of(context);
final SwitchThemeData switchTheme = SwitchTheme.of(context);
final _SwitchConfig switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
?? switchTheme.materialTapTargetSize
?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight);
return Size(switchConfig.switchWidth, switchConfig.switchHeight);
case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
return Size(switchConfig.switchWidth, switchConfig.switchHeightCollapsed);
}
}
......@@ -466,6 +509,7 @@ class Switch extends StatelessWidget {
onInactiveThumbImageError: onInactiveThumbImageError,
thumbColor: thumbColor,
trackColor: trackColor,
thumbIcon: thumbIcon,
materialTapTargetSize: materialTapTargetSize,
dragStartBehavior: dragStartBehavior,
mouseCursor: mouseCursor,
......@@ -524,6 +568,7 @@ class _MaterialSwitch extends StatefulWidget {
this.onInactiveThumbImageError,
this.thumbColor,
this.trackColor,
this.thumbIcon,
this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor,
......@@ -549,6 +594,7 @@ class _MaterialSwitch extends StatefulWidget {
final ImageErrorListener? onInactiveThumbImageError;
final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor;
final MaterialStateProperty<Icon?>? thumbIcon;
final MaterialTapTargetSize? materialTapTargetSize;
final DragStartBehavior dragStartBehavior;
final MouseCursor? mouseCursor;
......@@ -609,26 +655,8 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
});
}
MaterialStateProperty<Color> get _defaultThumbColor {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.grey.shade800 : Colors.grey.shade400;
}
if (states.contains(MaterialState.selected)) {
return theme.colorScheme.secondary;
}
return isDark ? Colors.grey.shade400 : Colors.grey.shade50;
});
}
MaterialStateProperty<Color?> get _widgetTrackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.inactiveTrackColor;
}
if (states.contains(MaterialState.selected)) {
return widget.activeTrackColor;
}
......@@ -636,28 +664,15 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
});
}
MaterialStateProperty<Color> get _defaultTrackColor {
final ThemeData theme = Theme.of(context);
final bool isDark = theme.brightness == Brightness.dark;
const Color black32 = Color(0x52000000); // Black with 32% opacity
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.white10 : Colors.black12;
}
if (states.contains(MaterialState.selected)) {
final Set<MaterialState> activeState = states..add(MaterialState.selected);
final Color activeColor = _widgetThumbColor.resolve(activeState) ?? _defaultThumbColor.resolve(activeState);
return activeColor.withAlpha(0x80);
}
return isDark ? Colors.white30 : black32;
});
}
double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
bool _isPressed = false;
void _handleDragStart(DragStartDetails details) {
if (isInteractive) {
setState(() {
_isPressed = true;
});
reactionController.forward();
}
}
......@@ -692,6 +707,9 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
} else {
animateToValue();
}
setState(() {
_isPressed = false;
});
reactionController.reverse();
}
......@@ -713,49 +731,66 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
final ThemeData theme = Theme.of(context);
final SwitchThemeData switchTheme = SwitchTheme.of(context);
final _SwitchConfig switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
final SwitchThemeData defaults = theme.useMaterial3 ? _SwitchDefaultsM3(context) : _SwitchDefaultsM2(context);
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected);
final Color effectiveActiveThumbColor = widget.thumbColor?.resolve(activeStates)
final Color? activeThumbColor = widget.thumbColor?.resolve(activeStates)
?? _widgetThumbColor.resolve(activeStates)
?? switchTheme.thumbColor?.resolve(activeStates)
?? _defaultThumbColor.resolve(activeStates);
final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
?? switchTheme.thumbColor?.resolve(activeStates);
final Color effectiveActiveThumbColor = activeThumbColor
?? defaults.thumbColor!.resolve(activeStates)!;
final Color? inactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
?? _widgetThumbColor.resolve(inactiveStates)
?? switchTheme.thumbColor?.resolve(inactiveStates)
?? _defaultThumbColor.resolve(inactiveStates);
?? switchTheme.thumbColor?.resolve(inactiveStates);
final Color effectiveInactiveThumbColor = inactiveThumbColor
?? defaults.thumbColor!.resolve(inactiveStates)!;
final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
?? _widgetTrackColor.resolve(activeStates)
?? switchTheme.trackColor?.resolve(activeStates)
?? _defaultTrackColor.resolve(activeStates);
?? _widgetThumbColor.resolve(activeStates)?.withAlpha(0x80)
?? defaults.trackColor!.resolve(activeStates)!;
final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates)
?? _widgetTrackColor.resolve(inactiveStates)
?? switchTheme.trackColor?.resolve(inactiveStates)
?? _defaultTrackColor.resolve(inactiveStates);
?? defaults.trackColor!.resolve(inactiveStates)!;
final Color? effectiveInactiveTrackOutlineColor = switchConfig.trackOutlineColor?.resolve(inactiveStates);
final Icon? effectiveActiveIcon = widget.thumbIcon?.resolve(activeStates)
?? switchTheme.thumbIcon?.resolve(activeStates);
final Icon? effectiveInactiveIcon = widget.thumbIcon?.resolve(inactiveStates)
?? switchTheme.thumbIcon?.resolve(inactiveStates);
final Color effectiveActiveIconColor = effectiveActiveIcon?.color ?? switchConfig.iconColor.resolve(activeStates);
final Color effectiveInactiveIconColor = effectiveInactiveIcon?.color ?? switchConfig.iconColor.resolve(inactiveStates);
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? switchTheme.overlayColor?.resolve(focusedStates)
?? theme.focusColor;
?? defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? switchTheme.overlayColor?.resolve(hoveredStates)
?? theme.hoverColor;
?? defaults.overlayColor!.resolve(hoveredStates)!;
final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
?? switchTheme.overlayColor?.resolve(activePressedStates)
?? effectiveActiveThumbColor.withAlpha(kRadialReactionAlpha);
?? activeThumbColor?.withAlpha(kRadialReactionAlpha)
?? defaults.overlayColor!.resolve(activePressedStates)!;
final Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates)
?? switchTheme.overlayColor?.resolve(inactivePressedStates)
?? effectiveInactiveThumbColor.withAlpha(kRadialReactionAlpha);
?? inactiveThumbColor?.withAlpha(kRadialReactionAlpha)
?? defaults.overlayColor!.resolve(inactivePressedStates)!;
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
......@@ -763,6 +798,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
});
final double effectiveActiveThumbRadius = effectiveActiveIcon == null ? switchConfig.activeThumbRadius : switchConfig.thumbRadiusWithIcon;
final double effectiveInactiveThumbRadius = effectiveInactiveIcon == null && widget.inactiveThumbImage == null
? switchConfig.inactiveThumbRadius : switchConfig.thumbRadiusWithIcon;
final double effectiveSplashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? defaults.splashRadius!;
return Semantics(
toggled: widget.value,
child: GestureDetector(
......@@ -785,10 +825,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? kRadialReactionRadius
..splashRadius = effectiveSplashRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..isPressed = _isPressed || downPosition != null
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activeThumbImage = widget.activeThumbImage
......@@ -797,11 +838,23 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor
..inactiveTrackColor = effectiveInactiveTrackColor
..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor
..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context)
..surfaceColor = theme.colorScheme.surface,
..surfaceColor = theme.colorScheme.surface
..inactiveThumbRadius = effectiveInactiveThumbRadius
..activeThumbRadius = effectiveActiveThumbRadius
..pressedThumbRadius = switchConfig.pressedThumbRadius
..trackHeight = switchConfig.trackHeight
..trackWidth = switchConfig.trackWidth
..activeIconColor = effectiveActiveIconColor
..inactiveIconColor = effectiveInactiveIconColor
..activeIcon = effectiveActiveIcon
..inactiveIcon = effectiveInactiveIcon
..iconTheme = IconTheme.of(context)
..thumbShadow = switchConfig.thumbShadow,
),
),
);
......@@ -809,6 +862,123 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
}
class _SwitchPainter extends ToggleablePainter {
Icon? get activeIcon => _activeIcon;
Icon? _activeIcon;
set activeIcon(Icon? value) {
if (value == _activeIcon) {
return;
}
_activeIcon = value;
notifyListeners();
}
Icon? get inactiveIcon => _inactiveIcon;
Icon? _inactiveIcon;
set inactiveIcon(Icon? value) {
if (value == _inactiveIcon) {
return;
}
_inactiveIcon = value;
notifyListeners();
}
IconThemeData? get iconTheme => _iconTheme;
IconThemeData? _iconTheme;
set iconTheme(IconThemeData? value) {
if (value == _iconTheme) {
return;
}
_iconTheme = value;
notifyListeners();
}
Color get activeIconColor => _activeIconColor!;
Color? _activeIconColor;
set activeIconColor(Color value) {
assert(value != null);
if (value == _activeIconColor) {
return;
}
_activeIconColor = value;
notifyListeners();
}
Color get inactiveIconColor => _inactiveIconColor!;
Color? _inactiveIconColor;
set inactiveIconColor(Color value) {
assert(value != null);
if (value == _inactiveIconColor) {
return;
}
_inactiveIconColor = value;
notifyListeners();
}
bool get isPressed => _isPressed!;
bool? _isPressed;
set isPressed(bool? value) {
if (value == _isPressed) {
return;
}
_isPressed = value;
notifyListeners();
}
double get activeThumbRadius => _activeThumbRadius!;
double? _activeThumbRadius;
set activeThumbRadius(double value) {
assert(value != null);
if (value == _activeThumbRadius) {
return;
}
_activeThumbRadius = value;
notifyListeners();
}
double get inactiveThumbRadius => _inactiveThumbRadius!;
double? _inactiveThumbRadius;
set inactiveThumbRadius(double value) {
assert(value != null);
if (value == _inactiveThumbRadius) {
return;
}
_inactiveThumbRadius = value;
notifyListeners();
}
double get pressedThumbRadius => _pressedThumbRadius!;
double? _pressedThumbRadius;
set pressedThumbRadius(double value) {
assert(value != null);
if (value == _pressedThumbRadius) {
return;
}
_pressedThumbRadius = value;
notifyListeners();
}
double get trackHeight => _trackHeight!;
double? _trackHeight;
set trackHeight(double value) {
assert(value != null);
if (value == _trackHeight) {
return;
}
_trackHeight = value;
notifyListeners();
}
double get trackWidth => _trackWidth!;
double? _trackWidth;
set trackWidth(double value) {
assert(value != null);
if (value == _trackWidth) {
return;
}
_trackWidth = value;
notifyListeners();
}
ImageProvider? get activeThumbImage => _activeThumbImage;
ImageProvider? _activeThumbImage;
set activeThumbImage(ImageProvider? value) {
......@@ -860,6 +1030,16 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners();
}
Color? get inactiveTrackOutlineColor => _inactiveTrackOutlineColor;
Color? _inactiveTrackOutlineColor;
set inactiveTrackOutlineColor(Color? value) {
if (value == _inactiveTrackOutlineColor) {
return;
}
_inactiveTrackOutlineColor = value;
notifyListeners();
}
Color get inactiveTrackColor => _inactiveTrackColor!;
Color? _inactiveTrackColor;
set inactiveTrackColor(Color value) {
......@@ -924,6 +1104,16 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners();
}
List<BoxShadow>? get thumbShadow => _thumbShadow;
List<BoxShadow>? _thumbShadow;
set thumbShadow(List<BoxShadow>? value) {
if (value == _thumbShadow) {
return;
}
_thumbShadow = value;
notifyListeners();
}
Color? _cachedThumbColor;
ImageProvider? _cachedThumbImage;
ImageErrorListener? _cachedThumbErrorListener;
......@@ -934,7 +1124,7 @@ class _SwitchPainter extends ToggleablePainter {
color: color,
image: image == null ? null : DecorationImage(image: image, onError: errorListener),
shape: BoxShape.circle,
boxShadow: kElevationToShadow[1],
boxShadow: thumbShadow,
);
}
......@@ -952,7 +1142,6 @@ class _SwitchPainter extends ToggleablePainter {
@override
void paint(Canvas canvas, Size size) {
final bool isEnabled = isInteractive;
final double currentValue = position.value;
final double visualPosition;
......@@ -965,29 +1154,32 @@ class _SwitchPainter extends ToggleablePainter {
break;
}
final double thumbRadius = isPressed
? pressedThumbRadius
: lerpDouble(inactiveThumbRadius, activeThumbRadius, currentValue)!;
final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!;
final Color? trackOutlineColor = inactiveTrackOutlineColor == null ? null
: Color.lerp(inactiveTrackOutlineColor, Colors.transparent, currentValue);
final Color lerpedThumbColor = Color.lerp(inactiveColor, activeColor, currentValue)!;
// Blend the thumb color against a `surfaceColor` background in case the
// thumbColor is not opaque. This way we do not see through the thumb to the
// track underneath.
final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor);
final ImageProvider? thumbImage = isEnabled
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
: inactiveThumbImage;
final Icon? thumbIcon = currentValue < 0.5 ? inactiveIcon : activeIcon;
final ImageErrorListener? thumbErrorListener = isEnabled
? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
: onInactiveThumbImageError;
final ImageProvider? thumbImage = currentValue < 0.5 ? inactiveThumbImage : activeThumbImage;
final ImageErrorListener? thumbErrorListener = currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError;
final Paint paint = Paint()
..color = trackColor;
final Offset trackPaintOffset = _computeTrackPaintOffset(size, _kTrackWidth, _kTrackHeight);
final Offset trackPaintOffset = _computeTrackPaintOffset(size, trackWidth, trackHeight);
final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, visualPosition);
final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + _kThumbRadius, size.height / 2);
final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + thumbRadius, size.height / 2);
_paintTrackWith(canvas, paint, trackPaintOffset);
_paintTrackWith(canvas, paint, trackPaintOffset, trackOutlineColor);
paintRadialReaction(canvas: canvas, origin: radialReactionOrigin);
_paintThumbWith(
thumbPaintOffset,
......@@ -996,13 +1188,15 @@ class _SwitchPainter extends ToggleablePainter {
thumbColor,
thumbImage,
thumbErrorListener,
thumbRadius,
thumbIcon,
);
}
/// Computes canvas offset for track's upper left corner
Offset _computeTrackPaintOffset(Size canvasSize, double trackWidth, double trackHeight) {
final double horizontalOffset = (canvasSize.width - _kTrackWidth) / 2.0;
final double verticalOffset = (canvasSize.height - _kTrackHeight) / 2.0;
final double horizontalOffset = (canvasSize.width - trackWidth) / 2.0;
final double verticalOffset = (canvasSize.height - trackHeight) / 2.0;
return Offset(horizontalOffset, verticalOffset);
}
......@@ -1011,7 +1205,9 @@ class _SwitchPainter extends ToggleablePainter {
/// square
Offset _computeThumbPaintOffset(Offset trackPaintOffset, double visualPosition) {
// How much thumb radius extends beyond the track
const double additionalThumbRadius = _kThumbRadius - _kTrackRadius;
final double trackRadius = trackHeight / 2;
final double thumbRadius = isPressed ? pressedThumbRadius : lerpDouble(inactiveThumbRadius, activeThumbRadius, position.value)!;
final double additionalThumbRadius = thumbRadius - trackRadius;
final double horizontalProgress = visualPosition * trackInnerLength;
final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius + horizontalProgress;
......@@ -1020,19 +1216,39 @@ class _SwitchPainter extends ToggleablePainter {
return Offset(thumbHorizontalOffset, thumbVerticalOffset);
}
void _paintTrackWith(Canvas canvas, Paint paint, Offset trackPaintOffset) {
void _paintTrackWith(Canvas canvas, Paint paint, Offset trackPaintOffset, Color? trackOutlineColor) {
final Rect trackRect = Rect.fromLTWH(
trackPaintOffset.dx,
trackPaintOffset.dy,
_kTrackWidth,
_kTrackHeight,
trackWidth,
trackHeight,
);
final double trackRadius = trackHeight / 2;
final RRect trackRRect = RRect.fromRectAndRadius(
trackRect,
const Radius.circular(_kTrackRadius),
Radius.circular(trackRadius),
);
canvas.drawRRect(trackRRect, paint);
if (trackOutlineColor != null) {
// paint track outline
final Rect outlineTrackRect = Rect.fromLTWH(
trackPaintOffset.dx + 1,
trackPaintOffset.dy + 1,
trackWidth - 2,
trackHeight - 2,
);
final RRect outlineTrackRRect = RRect.fromRectAndRadius(
outlineTrackRect,
Radius.circular(trackRadius),
);
final Paint outlinePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = trackOutlineColor;
canvas.drawRRect(outlineTrackRRect, outlinePaint);
}
}
void _paintThumbWith(
......@@ -1042,6 +1258,8 @@ class _SwitchPainter extends ToggleablePainter {
Color thumbColor,
ImageProvider? thumbImage,
ImageErrorListener? thumbErrorListener,
double thumbRadius,
Icon? thumbIcon,
) {
try {
_isPainting = true;
......@@ -1056,13 +1274,51 @@ class _SwitchPainter extends ToggleablePainter {
// The thumb contracts slightly during the animation
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
final double radius = _kThumbRadius - inset;
final double radius = thumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPaintOffset + Offset(0, inset),
configuration.copyWith(size: Size.fromRadius(radius)),
);
if (thumbIcon != null && thumbIcon.icon != null) {
final Color iconColor = Color.lerp(inactiveIconColor, activeIconColor, currentValue)!;
final double iconSize = thumbIcon.size ?? _SwitchConfigM3.iconSize;
final IconData iconData = thumbIcon.icon!;
final double? iconWeight = thumbIcon.weight ?? iconTheme?.weight;
final double? iconFill = thumbIcon.fill ?? iconTheme?.fill;
final double? iconGrade = thumbIcon.grade ?? iconTheme?.grade;
final double? iconOpticalSize = thumbIcon.opticalSize ?? iconTheme?.opticalSize;
final List<Shadow>? iconShadows = thumbIcon.shadows ?? iconTheme?.shadows;
final TextSpan textSpan = TextSpan(
text: String.fromCharCode(iconData.codePoint),
style: TextStyle(
fontVariations: <FontVariation>[
if (iconFill != null) FontVariation('FILL', iconFill),
if (iconWeight != null) FontVariation('wght', iconWeight),
if (iconGrade != null) FontVariation('GRAD', iconGrade),
if (iconOpticalSize != null) FontVariation('opsz', iconOpticalSize),
],
color: iconColor,
fontSize: iconSize,
inherit: false,
fontFamily: iconData.fontFamily,
package: iconData.fontPackage,
shadows: iconShadows,
),
);
final TextPainter textPainter = TextPainter(
textDirection: textDirection,
text: textSpan,
);
textPainter.layout();
final double additionalIconRadius = thumbRadius - iconSize / 2;
final Offset offset = thumbPaintOffset + Offset(additionalIconRadius, additionalIconRadius);
textPainter.paint(canvas, offset);
}
} finally {
_isPainting = false;
}
......@@ -1078,3 +1334,330 @@ class _SwitchPainter extends ToggleablePainter {
super.dispose();
}
}
mixin _SwitchConfig {
double get trackHeight;
double get trackWidth;
double get switchWidth;
double get switchHeight;
double get switchHeightCollapsed;
double get activeThumbRadius;
double get inactiveThumbRadius;
double get pressedThumbRadius;
double get thumbRadiusWithIcon;
List<BoxShadow>? get thumbShadow;
MaterialStateProperty<Color?>? get trackOutlineColor;
MaterialStateProperty<Color> get iconColor;
}
// Hand coded defaults based on Material Design 2.
class _SwitchConfigM2 with _SwitchConfig {
_SwitchConfigM2();
@override
double get activeThumbRadius => 10.0;
@override
MaterialStateProperty<Color> get iconColor => MaterialStateProperty.all<Color>(Colors.transparent);
@override
double get inactiveThumbRadius => 10.0;
@override
double get pressedThumbRadius => 10.0;
@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 => 10.0;
@override
List<BoxShadow>? get thumbShadow => kElevationToShadow[1];
@override
double get trackHeight => 14.0;
@override
MaterialStateProperty<Color?>? get trackOutlineColor => null;
@override
double get trackWidth => 33.0;
}
class _SwitchDefaultsM2 extends SwitchThemeData {
_SwitchDefaultsM2(BuildContext context)
: _theme = Theme.of(context),
_colors = Theme.of(context).colorScheme;
final ThemeData _theme;
final ColorScheme _colors;
@override
MaterialStateProperty<Color> get thumbColor {
final bool isDark = _theme.brightness == Brightness.dark;
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.grey.shade800 : Colors.grey.shade400;
}
if (states.contains(MaterialState.selected)) {
return _colors.secondary;
}
return isDark ? Colors.grey.shade400 : Colors.grey.shade50;
});
}
@override
MaterialStateProperty<Color> get trackColor {
final bool isDark = _theme.brightness == Brightness.dark;
const Color black32 = Color(0x52000000); // Black with 32% opacity
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return isDark ? Colors.white10 : Colors.black12;
}
if (states.contains(MaterialState.selected)) {
final Color activeColor = _colors.secondary;
return activeColor.withAlpha(0x80);
}
return isDark ? Colors.white30 : black32;
});
}
@override
MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;
@override
MaterialStateProperty<MouseCursor> get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) => MaterialStateMouseCursor.clickable.resolve(states));
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return thumbColor.resolve(states).withAlpha(kRadialReactionAlpha);
}
if (states.contains(MaterialState.focused)) {
return _theme.focusColor;
}
if (states.contains(MaterialState.hovered)) {
return _theme.hoverColor;
}
return null;
});
}
@override
double get splashRadius => kRadialReactionRadius;
}
// BEGIN GENERATED TOKEN PROPERTIES - Switch
// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
// dev/tools/gen_defaults/bin/gen_defaults.dart.
// Token database version: v0_101
class _SwitchDefaultsM3 extends SwitchThemeData {
_SwitchDefaultsM3(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 _colors.surface.withOpacity(1.0);
}
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.primaryContainer;
}
if (states.contains(MaterialState.hovered)) {
return _colors.primaryContainer;
}
if (states.contains(MaterialState.focused)) {
return _colors.primaryContainer;
}
return _colors.onPrimary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurfaceVariant;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurfaceVariant;
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurfaceVariant;
}
return _colors.outline;
});
}
@override
MaterialStateProperty<Color> get trackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return _colors.onSurface.withOpacity(0.12);
}
return _colors.surfaceVariant.withOpacity(0.12);
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.primary;
}
if (states.contains(MaterialState.hovered)) {
return _colors.primary;
}
if (states.contains(MaterialState.focused)) {
return _colors.primary;
}
return _colors.primary;
}
if (states.contains(MaterialState.pressed)) {
return _colors.surfaceVariant;
}
if (states.contains(MaterialState.hovered)) {
return _colors.surfaceVariant;
}
if (states.contains(MaterialState.focused)) {
return _colors.surfaceVariant;
}
return _colors.surfaceVariant;
});
}
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.primary.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.primary.withOpacity(0.12);
}
return null;
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface.withOpacity(0.12);
}
return null;
});
}
@override
double get splashRadius => 40.0 / 2;
}
class _SwitchConfigM3 with _SwitchConfig {
_SwitchConfigM3(this.context)
: _colors = Theme.of(context).colorScheme;
BuildContext context;
final ColorScheme _colors;
static const double iconSize = 16.0;
@override
double get activeThumbRadius => 24.0 / 2;
@override
MaterialStateProperty<Color> get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.surfaceVariant.withOpacity(0.38);
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return _colors.onPrimaryContainer;
}
if (states.contains(MaterialState.hovered)) {
return _colors.onPrimaryContainer;
}
if (states.contains(MaterialState.focused)) {
return _colors.onPrimaryContainer;
}
return _colors.onPrimaryContainer;
}
if (states.contains(MaterialState.pressed)) {
return _colors.surfaceVariant;
}
if (states.contains(MaterialState.hovered)) {
return _colors.surfaceVariant;
}
if (states.contains(MaterialState.focused)) {
return _colors.surfaceVariant;
}
return _colors.surfaceVariant;
});
}
@override
double get inactiveThumbRadius => 16.0 / 2;
@override
double get pressedThumbRadius => 28.0 / 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 => 24.0 / 2;
@override
List<BoxShadow>? get thumbShadow => kElevationToShadow[0];
@override
double get trackHeight => 32.0;
@override
MaterialStateProperty<Color?> get trackOutlineColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return null;
}
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.12);
}
return _colors.outline;
});
}
@override
double get trackWidth => 52.0;
}
// END GENERATED TOKEN PROPERTIES - Switch
......@@ -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));
}
}
......
......@@ -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.
......
......@@ -21,6 +21,8 @@ import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
void main() {
final ThemeData theme = ThemeData();
testWidgets('Switch can toggle on tap', (WidgetTester tester) async {
final Key switchKey = UniqueKey();
bool value = false;
......@@ -30,7 +32,9 @@ void main() {
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Switch(
dragStartBehavior: DragStartBehavior.down,
......@@ -43,6 +47,7 @@ void main() {
},
),
),
),
);
},
),
......@@ -55,9 +60,10 @@ void main() {
});
testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget(
Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
......@@ -73,11 +79,14 @@ void main() {
),
);
expect(tester.getSize(find.byType(Switch)), const Size(59.0, 48.0));
// switch width = trackWidth - 2 * trackRadius + _kSwitchMinSize
// M2 width = 33 - 2 * 7 + 40
// M3 width = 52 - 2 * 16 + 40
expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 48.0) : const Size(59.0, 48.0));
await tester.pumpWidget(
Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
......@@ -93,10 +102,10 @@ void main() {
),
);
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));
});
testWidgets('Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async {
testWidgets('Switch does not get distorted upon changing constraints with parent - M2', (WidgetTester tester) async {
const double maxWidth = 300;
const double maxHeight = 100;
......@@ -151,7 +160,9 @@ void main() {
bool value = false;
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -171,6 +182,7 @@ void main() {
},
),
),
),
);
expect(value, isFalse);
......@@ -198,7 +210,9 @@ void main() {
bool value = false;
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -218,6 +232,7 @@ void main() {
},
),
),
),
);
expect(value, isFalse);
......@@ -287,7 +302,9 @@ void main() {
bool value = false;
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -307,6 +324,7 @@ void main() {
},
),
),
),
);
await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));
......@@ -513,7 +531,9 @@ void main() {
bool value = false;
await tester.pumpWidget(
Directionality(
MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -533,6 +553,7 @@ void main() {
},
),
),
),
);
expect(value, isFalse);
......@@ -554,7 +575,9 @@ void main() {
bool value = false;
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -574,6 +597,7 @@ void main() {
},
),
),
),
);
// Move a little to the right, not past the middle.
......@@ -637,7 +661,9 @@ void main() {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -656,6 +682,7 @@ void main() {
},
),
),
),
);
await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Switch));
......@@ -682,6 +709,7 @@ void main() {
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
void onChanged(bool newValue) {
......@@ -859,6 +887,7 @@ void main() {
const double splashRadius = 30;
Widget buildApp() {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
......@@ -962,6 +991,7 @@ void main() {
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
......@@ -1001,6 +1031,7 @@ void main() {
// Test Switch.adaptive() constructor
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
......@@ -1029,6 +1060,7 @@ void main() {
// Test Switch() constructor
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
......@@ -1053,6 +1085,7 @@ void main() {
// Test default cursor
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Scaffold(
body: Align(
alignment: Alignment.topLeft,
......@@ -1074,8 +1107,9 @@ void main() {
// Test default cursor when disabled
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
MaterialApp(
theme: theme,
home: const Scaffold(
body: Align(
alignment: Alignment.topLeft,
child: Material(
......@@ -1103,7 +1137,9 @@ void main() {
bool enabled = true;
late StateSetter stateSetter;
await tester.pumpWidget(
Directionality(
Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
......@@ -1123,6 +1159,7 @@ void main() {
},
),
),
),
);
final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
......@@ -1580,6 +1617,7 @@ void main() {
Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) {
return MaterialApp(
theme: theme,
home: Scaffold(
body: Switch(
focusNode: focusNode,
......@@ -1701,6 +1739,7 @@ void main() {
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
Widget buildSwitch(bool show) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: show ? Switch(value: true, onChanged: (_) { }) : Container(),
......@@ -1785,11 +1824,53 @@ void main() {
image = await createTestImage(width: 100, height: 100);
});
testWidgets('thumb image shows up', (WidgetTester tester) async {
imageCache.clear();
final _TestImageProvider provider1 = _TestImageProvider();
final _TestImageProvider provider2 = _TestImageProvider();
expect(provider1.loadCallCount, 0);
expect(provider2.loadCallCount, 0);
bool value1 = true;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.android),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Switch(
activeThumbImage: provider1,
inactiveThumbImage: provider2,
value: value1,
onChanged: (bool val) {
setState(() {
value1 = val;
});
},
),
);
}
)
)
);
expect(provider1.loadCallCount, 1);
expect(provider2.loadCallCount, 0);
expect(imageCache.liveImageCount, 1);
await tester.tap(find.byType(Switch));
await tester.pumpAndSettle();
expect(provider1.loadCallCount, 1);
expect(provider2.loadCallCount, 1);
expect(imageCache.liveImageCount, 2);
});
testWidgets('do not crash when imageProvider completes after Switch is disposed', (WidgetTester tester) async {
final DelayedImageProvider imageProvider = DelayedImageProvider(image);
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Switch(
......@@ -1819,6 +1900,7 @@ void main() {
Future<void> buildSwitch(ImageProvider imageProvider) {
return tester.pumpWidget(
MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Switch(
......@@ -1850,27 +1932,933 @@ void main() {
expect(tester.takeException(), isNull);
});
});
}
class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
DelayedImageProvider(this.image);
group('Switch M3 tests', () {
testWidgets('Switch has default colors when enabled - M3', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = theme.colorScheme;
bool value = false;
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
dragStartBehavior: DragStartBehavior.down,
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
final ui.Image image;
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..save()
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.outline,
rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
)
..circle(color: colors.outline), // thumb color
reason: 'Inactive enabled switch should match these colors',
);
await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
await tester.pump();
final Completer<ImageInfo> _completer = Completer<ImageInfo>();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..save()
..rrect(
style: PaintingStyle.fill,
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.onPrimary), // thumb color
reason: 'Active enabled switch should match these colors',
);
});
@override
Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<DelayedImageProvider>(this);
}
testWidgets('Inactive Switch has default colors when disabled - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
@override
ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(_completer.future);
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return const Material(
child: Center(
child: Switch(
value: false,
onChanged: null,
),
),
);
},
),
),
));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..save()
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
)
..circle(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), // thumb color
reason: 'Inactive disabled switch should match these colors',
);
});
testWidgets('Active Switch has default colors when disabled - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true,
colorSchemeSeed: const Color(0xff6750a4),
brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return const Material(
child: Center(
child: Switch(
value: true,
onChanged: null,
),
),
);
},
),
),
));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..save()
..rrect(
style: PaintingStyle.fill,
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.surface), // thumb color
reason: 'Active disabled switch should match these colors',
);
});
testWidgets('Switch can be set color - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
bool value = false;
await tester.pumpWidget(
MaterialApp(
theme: themeData,
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
dragStartBehavior: DragStartBehavior.down,
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
activeColor: Colors.red[500],
activeTrackColor: Colors.green[500],
inactiveThumbColor: Colors.yellow[500],
inactiveTrackColor: Colors.blue[500],
),
),
);
},
),
),
),
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: Colors.blue[500],
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.outline,
)
..circle(color: Colors.yellow[500]), // thumb color
);
await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
await tester.pump();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: Colors.green[500],
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: Colors.red[500]), // thumb color
);
});
testWidgets('Switch is focusable and has correct focus color - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
focusColor: Colors.orange[500],
autofocus: true,
focusNode: focusNode,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
Future<void> complete() async {
_completer.complete(ImageInfo(image: image));
// active, enabled switch
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: Colors.orange[500]),
);
// Check the false value: inactive enabled switch
value = false;
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.outline,
rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
)
..circle(color: Colors.orange[500])
);
// Check what happens when disabled: inactive disabled switch.
value = false;
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
)
..circle(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)),
);
});
testWidgets('Switch can be hovered and has correct hover color - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
Widget buildApp({bool enabled = true}) {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return Switch(
value: value,
onChanged: enabled ? (bool newValue) {
setState(() {
value = newValue;
});
} : null,
hoverColor: Colors.orange[500],
);
}),
),
),
);
}
// active enabled switch
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.onPrimary),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: Colors.orange[500]),
);
// Check what happens for disabled active switch
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.surface.withOpacity(1.0)),
);
});
testWidgets('Switch thumb color resolves in active/enabled states - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
const Color activeEnabledThumbColor = Color(0xFF000001);
const Color activeDisabledThumbColor = Color(0xFF000002);
const Color inactiveEnabledThumbColor = Color(0xFF000003);
const Color inactiveDisabledThumbColor = Color(0xFF000004);
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledThumbColor;
}
return inactiveDisabledThumbColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledThumbColor;
}
return inactiveEnabledThumbColor;
}
final MaterialStateProperty<Color> thumbColor = MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Theme(
data: themeData,
child: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
thumbColor: thumbColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..rrect(
style: PaintingStyle.stroke,
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
)
..circle(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: activeDisabledThumbColor),
reason: 'Active disabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.surfaceVariant,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: inactiveEnabledThumbColor),
reason: 'Inactive enabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: activeEnabledThumbColor),
reason: 'Active enabled switch should match these colors',
);
});
testWidgets('Switch thumb color resolves in hovered/focused states - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final ColorScheme colors = themeData.colorScheme;
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredThumbColor = Color(0xFF000001);
const Color focusedThumbColor = Color(0xFF000002);
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredThumbColor;
}
if (states.contains(MaterialState.focused)) {
return focusedThumbColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> thumbColor = MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch() {
return MaterialApp(
theme: themeData,
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
focusNode: focusNode,
autofocus: true,
value: true,
thumbColor: thumbColor,
onChanged: (_) { },
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildSwitch());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.primary.withOpacity(0.12))
..circle(color: focusedThumbColor),
reason: 'active enabled switch should default track and custom thumb color',
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
style: PaintingStyle.fill,
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.primary.withOpacity(0.08))
..circle(color: hoveredThumbColor),
reason: 'active enabled switch should default track and custom thumb color',
);
});
testWidgets('Track color resolves in active/enabled states - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
const Color activeEnabledTrackColor = Color(0xFF000001);
const Color activeDisabledTrackColor = Color(0xFF000002);
const Color inactiveEnabledTrackColor = Color(0xFF000003);
const Color inactiveDisabledTrackColor = Color(0xFF000004);
Color getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return activeDisabledTrackColor;
}
return inactiveDisabledTrackColor;
}
if (states.contains(MaterialState.selected)) {
return activeEnabledTrackColor;
}
return inactiveEnabledTrackColor;
}
final MaterialStateProperty<Color> trackColor =
MaterialStateColor.resolveWith(getTrackColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Theme(
data: themeData,
child: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
trackColor: trackColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: false));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveDisabledTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Inactive disabled switch track should use this value',
);
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: activeDisabledTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Active disabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveEnabledTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Inactive enabled switch should match these colors',
);
await tester.pumpWidget(buildSwitch(enabled: true, active: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: activeEnabledTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Active enabled switch should match these colors',
);
});
testWidgets('Switch track color resolves in hovered/focused states', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredTrackColor = Color(0xFF000001);
const Color focusedTrackColor = Color(0xFF000002);
Color getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredTrackColor;
}
if (states.contains(MaterialState.focused)) {
return focusedTrackColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> trackColor =
MaterialStateColor.resolveWith(getTrackColor);
Widget buildSwitch() {
return Theme(
data: themeData,
child: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
focusNode: focusNode,
autofocus: true,
value: true,
trackColor: trackColor,
onChanged: (_) { },
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildSwitch());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: focusedTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Active enabled switch should match these colors',
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: hoveredTrackColor,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
),
reason: 'Active enabled switch should match these colors',
);
});
testWidgets('Switch thumb color is blended against surface color - M3', (WidgetTester tester) async {
final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60);
final ThemeData theme = ThemeData.light(useMaterial3: true);
final ColorScheme colors = theme.colorScheme;
Color getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return activeDisabledThumbColor;
}
return Colors.black;
}
final MaterialStateProperty<Color> thumbColor =
MaterialStateColor.resolveWith(getThumbColor);
Widget buildSwitch({required bool enabled, required bool active}) {
return Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Theme(
data: theme,
child: Material(
child: Center(
child: Switch(
thumbColor: thumbColor,
value: active,
onChanged: enabled ? (_) { } : null,
),
),
),
);
},
),
);
}
await tester.pumpWidget(buildSwitch(enabled: false, active: true));
final Color expectedThumbColor = Color.alphaBlend(activeDisabledThumbColor, theme.colorScheme.surface);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: colors.onSurface.withOpacity(0.12),
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: expectedThumbColor),
reason: 'Active disabled thumb color should be blended on top of surface color',
);
});
testWidgets('Switch can set icon - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xff6750a4),
brightness: Brightness.light);
MaterialStateProperty<Icon?> thumbIcon(Icon? activeIcon, Icon? inactiveIcon) {
return MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return activeIcon;
}
return inactiveIcon;
});
}
Widget buildSwitch({required bool enabled, required bool active, Icon? activeIcon, Icon? inactiveIcon}) {
return Theme(
data: themeData,
child: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
thumbIcon: thumbIcon(activeIcon, inactiveIcon),
value: active,
onChanged: enabled ? (_) {} : null,
),
),
);
},
),
),
);
}
// active icon shows when switch is on.
await tester.pumpWidget(buildSwitch(enabled: true, active: true, activeIcon: const Icon(Icons.close)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..circle()
..paragraph(offset: const Offset(32.0, 16.0)),
);
// inactive icon shows when switch is off.
await tester.pumpWidget(buildSwitch(enabled: true, active: false, inactiveIcon: const Icon(Icons.close)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()
..circle()
..paragraph(offset: const Offset(12.0, 16.0)),
);
// active icon doesn't show when switch is off.
await tester.pumpWidget(buildSwitch(enabled: true, active: false, activeIcon: const Icon(Icons.check)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()..circle()
);
// inactive icon doesn't show when switch is on.
await tester.pumpWidget(buildSwitch(enabled: true, active: true, inactiveIcon: const Icon(Icons.check)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..circle()..restore(),
);
// without icon
await tester.pumpWidget(buildSwitch(enabled: true, active: false));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()..circle()..restore(),
);
});
});
}
class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
DelayedImageProvider(this.image);
final ui.Image image;
final Completer<ImageInfo> _completer = Completer<ImageInfo>();
@override
Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<DelayedImageProvider>(this);
}
@override
ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
return OneFrameImageStreamCompleter(_completer.future);
}
Future<void> complete() async {
_completer.complete(ImageInfo(image: image));
}
@override
String toString() => '${describeIdentity(this)}()';
}
class _TestImageProvider extends ImageProvider<Object> {
_TestImageProvider({ImageStreamCompleter? streamCompleter}) {
_streamCompleter = streamCompleter
?? OneFrameImageStreamCompleter(_completer.future);
}
final Completer<ImageInfo> _completer = Completer<ImageInfo>();
late ImageStreamCompleter _streamCompleter;
bool get loadCalled => _loadCallCount > 0;
int get loadCallCount => _loadCallCount;
int _loadCallCount = 0;
@override
Future<Object> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<_TestImageProvider>(this);
}
@override
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) {
super.resolveStreamForKey(configuration, stream, key, handleError);
}
@override
ImageStreamCompleter load(Object key, DecoderCallback decode) {
_loadCallCount += 1;
return _streamCompleter;
}
void complete(ui.Image image) {
_completer.complete(ImageInfo(image: image));
}
void fail(Object exception, StackTrace? stackTrace) {
_completer.completeError(exception, stackTrace);
}
@override
......
......@@ -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))
);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment