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'; ...@@ -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_bar_template.dart';
import 'package:gen_defaults/navigation_rail_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/surface_tint.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/text_field_template.dart';
import 'package:gen_defaults/typography_template.dart'; import 'package:gen_defaults/typography_template.dart';
...@@ -121,6 +122,7 @@ Future<void> main(List<String> args) async { ...@@ -121,6 +122,7 @@ Future<void> main(List<String> args) async {
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile(); NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.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(); TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
TypographyTemplate('Typography', '$materialLib/typography.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 @@ ...@@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'color_scheme.dart';
import 'colors.dart'; import 'colors.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
...@@ -21,14 +24,7 @@ import 'toggleable.dart'; ...@@ -21,14 +24,7 @@ import 'toggleable.dart';
// bool _giveVerse = true; // bool _giveVerse = true;
// late StateSetter setState; // 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 _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 } enum _SwitchType { material, adaptive }
...@@ -48,6 +44,10 @@ enum _SwitchType { material, adaptive } ...@@ -48,6 +44,10 @@ enum _SwitchType { material, adaptive }
/// ///
/// Requires one of its ancestors to be a [Material] widget. /// 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} /// {@tool dartpad}
/// This example shows a toggleable [Switch]. When the thumb slides to the other /// This example shows a toggleable [Switch]. When the thumb slides to the other
/// side of the track, the switch is toggled between on/off. /// side of the track, the switch is toggled between on/off.
...@@ -62,6 +62,13 @@ enum _SwitchType { material, adaptive } ...@@ -62,6 +62,13 @@ enum _SwitchType { material, adaptive }
/// ** See code in examples/api/lib/material/switch/switch.1.dart ** /// ** See code in examples/api/lib/material/switch/switch.1.dart **
/// {@end-tool} /// {@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: /// See also:
/// ///
/// * [SwitchListTile], which combines this widget with a [ListTile] so that /// * [SwitchListTile], which combines this widget with a [ListTile] so that
...@@ -98,6 +105,7 @@ class Switch extends StatelessWidget { ...@@ -98,6 +105,7 @@ class Switch extends StatelessWidget {
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.thumbColor, this.thumbColor,
this.trackColor, this.trackColor,
this.thumbIcon,
this.materialTapTargetSize, this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor, this.mouseCursor,
...@@ -142,6 +150,7 @@ class Switch extends StatelessWidget { ...@@ -142,6 +150,7 @@ class Switch extends StatelessWidget {
this.materialTapTargetSize, this.materialTapTargetSize,
this.thumbColor, this.thumbColor,
this.trackColor, this.trackColor,
this.thumbIcon,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor, this.mouseCursor,
this.focusColor, this.focusColor,
...@@ -322,6 +331,39 @@ class Switch extends StatelessWidget { ...@@ -322,6 +331,39 @@ class Switch extends StatelessWidget {
/// | Disabled | `Colors.black12` | `Colors.white10` | /// | Disabled | `Colors.black12` | `Colors.white10` |
final MaterialStateProperty<Color?>? trackColor; 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} /// {@template flutter.material.switch.materialTapTargetSize}
/// Configures the minimum size of the tap target. /// Configures the minimum size of the tap target.
/// {@endtemplate} /// {@endtemplate}
...@@ -419,15 +461,16 @@ class Switch extends StatelessWidget { ...@@ -419,15 +461,16 @@ class Switch extends StatelessWidget {
Size _getSwitchSize(BuildContext context) { Size _getSwitchSize(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final SwitchThemeData switchTheme = SwitchTheme.of(context); final SwitchThemeData switchTheme = SwitchTheme.of(context);
final _SwitchConfig switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
?? switchTheme.materialTapTargetSize ?? switchTheme.materialTapTargetSize
?? theme.materialTapTargetSize; ?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) { switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded: case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight); return Size(switchConfig.switchWidth, switchConfig.switchHeight);
case MaterialTapTargetSize.shrinkWrap: case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed); return Size(switchConfig.switchWidth, switchConfig.switchHeightCollapsed);
} }
} }
...@@ -466,6 +509,7 @@ class Switch extends StatelessWidget { ...@@ -466,6 +509,7 @@ class Switch extends StatelessWidget {
onInactiveThumbImageError: onInactiveThumbImageError, onInactiveThumbImageError: onInactiveThumbImageError,
thumbColor: thumbColor, thumbColor: thumbColor,
trackColor: trackColor, trackColor: trackColor,
thumbIcon: thumbIcon,
materialTapTargetSize: materialTapTargetSize, materialTapTargetSize: materialTapTargetSize,
dragStartBehavior: dragStartBehavior, dragStartBehavior: dragStartBehavior,
mouseCursor: mouseCursor, mouseCursor: mouseCursor,
...@@ -524,6 +568,7 @@ class _MaterialSwitch extends StatefulWidget { ...@@ -524,6 +568,7 @@ class _MaterialSwitch extends StatefulWidget {
this.onInactiveThumbImageError, this.onInactiveThumbImageError,
this.thumbColor, this.thumbColor,
this.trackColor, this.trackColor,
this.thumbIcon,
this.materialTapTargetSize, this.materialTapTargetSize,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.mouseCursor, this.mouseCursor,
...@@ -549,6 +594,7 @@ class _MaterialSwitch extends StatefulWidget { ...@@ -549,6 +594,7 @@ class _MaterialSwitch extends StatefulWidget {
final ImageErrorListener? onInactiveThumbImageError; final ImageErrorListener? onInactiveThumbImageError;
final MaterialStateProperty<Color?>? thumbColor; final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor; final MaterialStateProperty<Color?>? trackColor;
final MaterialStateProperty<Icon?>? thumbIcon;
final MaterialTapTargetSize? materialTapTargetSize; final MaterialTapTargetSize? materialTapTargetSize;
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
final MouseCursor? mouseCursor; final MouseCursor? mouseCursor;
...@@ -609,26 +655,8 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -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 { MaterialStateProperty<Color?> get _widgetTrackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return widget.inactiveTrackColor;
}
if (states.contains(MaterialState.selected)) { if (states.contains(MaterialState.selected)) {
return widget.activeTrackColor; return widget.activeTrackColor;
} }
...@@ -636,28 +664,15 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -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; double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
bool _isPressed = false;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
if (isInteractive) { if (isInteractive) {
setState(() {
_isPressed = true;
});
reactionController.forward(); reactionController.forward();
} }
} }
...@@ -692,6 +707,9 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -692,6 +707,9 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
} else { } else {
animateToValue(); animateToValue();
} }
setState(() {
_isPressed = false;
});
reactionController.reverse(); reactionController.reverse();
} }
...@@ -713,49 +731,66 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -713,49 +731,66 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final SwitchThemeData switchTheme = SwitchTheme.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 // Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between. // so that they can be lerped between.
final Set<MaterialState> activeStates = states..add(MaterialState.selected); final Set<MaterialState> activeStates = states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(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) ?? _widgetThumbColor.resolve(activeStates)
?? switchTheme.thumbColor?.resolve(activeStates) ?? switchTheme.thumbColor?.resolve(activeStates);
?? _defaultThumbColor.resolve(activeStates); final Color effectiveActiveThumbColor = activeThumbColor
final Color effectiveInactiveThumbColor = widget.thumbColor?.resolve(inactiveStates) ?? defaults.thumbColor!.resolve(activeStates)!;
final Color? inactiveThumbColor = widget.thumbColor?.resolve(inactiveStates)
?? _widgetThumbColor.resolve(inactiveStates) ?? _widgetThumbColor.resolve(inactiveStates)
?? switchTheme.thumbColor?.resolve(inactiveStates) ?? switchTheme.thumbColor?.resolve(inactiveStates);
?? _defaultThumbColor.resolve(inactiveStates); final Color effectiveInactiveThumbColor = inactiveThumbColor
?? defaults.thumbColor!.resolve(inactiveStates)!;
final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates) final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
?? _widgetTrackColor.resolve(activeStates) ?? _widgetTrackColor.resolve(activeStates)
?? switchTheme.trackColor?.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) final Color effectiveInactiveTrackColor = widget.trackColor?.resolve(inactiveStates)
?? _widgetTrackColor.resolve(inactiveStates) ?? _widgetTrackColor.resolve(inactiveStates)
?? switchTheme.trackColor?.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 Set<MaterialState> focusedStates = states..add(MaterialState.focused);
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor ?? widget.focusColor
?? switchTheme.overlayColor?.resolve(focusedStates) ?? switchTheme.overlayColor?.resolve(focusedStates)
?? theme.focusColor; ?? defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered); final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor ?? widget.hoverColor
?? switchTheme.overlayColor?.resolve(hoveredStates) ?? switchTheme.overlayColor?.resolve(hoveredStates)
?? theme.hoverColor; ?? defaults.overlayColor!.resolve(hoveredStates)!;
final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed); final Set<MaterialState> activePressedStates = activeStates..add(MaterialState.pressed);
final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates)
?? switchTheme.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 Set<MaterialState> inactivePressedStates = inactiveStates..add(MaterialState.pressed);
final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates)
?? switchTheme.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) { final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states) return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
...@@ -763,6 +798,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -763,6 +798,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states); ?? 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( return Semantics(
toggled: widget.value, toggled: widget.value,
child: GestureDetector( child: GestureDetector(
...@@ -785,10 +825,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -785,10 +825,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..reactionColor = effectiveActivePressedOverlayColor ..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor ..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor ..focusColor = effectiveFocusOverlayColor
..splashRadius = widget.splashRadius ?? switchTheme.splashRadius ?? kRadialReactionRadius ..splashRadius = effectiveSplashRadius
..downPosition = downPosition ..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused) ..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered) ..isHovered = states.contains(MaterialState.hovered)
..isPressed = _isPressed || downPosition != null
..activeColor = effectiveActiveThumbColor ..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor ..inactiveColor = effectiveInactiveThumbColor
..activeThumbImage = widget.activeThumbImage ..activeThumbImage = widget.activeThumbImage
...@@ -797,11 +838,23 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ...@@ -797,11 +838,23 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..onInactiveThumbImageError = widget.onInactiveThumbImageError ..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor ..activeTrackColor = effectiveActiveTrackColor
..inactiveTrackColor = effectiveInactiveTrackColor ..inactiveTrackColor = effectiveInactiveTrackColor
..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor
..configuration = createLocalImageConfiguration(context) ..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive ..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength ..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context) ..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 ...@@ -809,6 +862,123 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
} }
class _SwitchPainter extends ToggleablePainter { 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? get activeThumbImage => _activeThumbImage;
ImageProvider? _activeThumbImage; ImageProvider? _activeThumbImage;
set activeThumbImage(ImageProvider? value) { set activeThumbImage(ImageProvider? value) {
...@@ -860,6 +1030,16 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -860,6 +1030,16 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners(); notifyListeners();
} }
Color? get inactiveTrackOutlineColor => _inactiveTrackOutlineColor;
Color? _inactiveTrackOutlineColor;
set inactiveTrackOutlineColor(Color? value) {
if (value == _inactiveTrackOutlineColor) {
return;
}
_inactiveTrackOutlineColor = value;
notifyListeners();
}
Color get inactiveTrackColor => _inactiveTrackColor!; Color get inactiveTrackColor => _inactiveTrackColor!;
Color? _inactiveTrackColor; Color? _inactiveTrackColor;
set inactiveTrackColor(Color value) { set inactiveTrackColor(Color value) {
...@@ -924,6 +1104,16 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -924,6 +1104,16 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners(); notifyListeners();
} }
List<BoxShadow>? get thumbShadow => _thumbShadow;
List<BoxShadow>? _thumbShadow;
set thumbShadow(List<BoxShadow>? value) {
if (value == _thumbShadow) {
return;
}
_thumbShadow = value;
notifyListeners();
}
Color? _cachedThumbColor; Color? _cachedThumbColor;
ImageProvider? _cachedThumbImage; ImageProvider? _cachedThumbImage;
ImageErrorListener? _cachedThumbErrorListener; ImageErrorListener? _cachedThumbErrorListener;
...@@ -934,7 +1124,7 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -934,7 +1124,7 @@ class _SwitchPainter extends ToggleablePainter {
color: color, color: color,
image: image == null ? null : DecorationImage(image: image, onError: errorListener), image: image == null ? null : DecorationImage(image: image, onError: errorListener),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: kElevationToShadow[1], boxShadow: thumbShadow,
); );
} }
...@@ -952,7 +1142,6 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -952,7 +1142,6 @@ class _SwitchPainter extends ToggleablePainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final bool isEnabled = isInteractive;
final double currentValue = position.value; final double currentValue = position.value;
final double visualPosition; final double visualPosition;
...@@ -965,29 +1154,32 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -965,29 +1154,32 @@ class _SwitchPainter extends ToggleablePainter {
break; break;
} }
final double thumbRadius = isPressed
? pressedThumbRadius
: lerpDouble(inactiveThumbRadius, activeThumbRadius, currentValue)!;
final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, 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)!; final Color lerpedThumbColor = Color.lerp(inactiveColor, activeColor, currentValue)!;
// Blend the thumb color against a `surfaceColor` background in case the // 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 // thumbColor is not opaque. This way we do not see through the thumb to the
// track underneath. // track underneath.
final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor); final Color thumbColor = Color.alphaBlend(lerpedThumbColor, surfaceColor);
final ImageProvider? thumbImage = isEnabled final Icon? thumbIcon = currentValue < 0.5 ? inactiveIcon : activeIcon;
? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage)
: inactiveThumbImage;
final ImageErrorListener? thumbErrorListener = isEnabled final ImageProvider? thumbImage = currentValue < 0.5 ? inactiveThumbImage : activeThumbImage;
? (currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError)
: onInactiveThumbImageError; final ImageErrorListener? thumbErrorListener = currentValue < 0.5 ? onInactiveThumbImageError : onActiveThumbImageError;
final Paint paint = Paint() final Paint paint = Paint()
..color = trackColor; ..color = trackColor;
final Offset trackPaintOffset = _computeTrackPaintOffset(size, _kTrackWidth, _kTrackHeight); final Offset trackPaintOffset = _computeTrackPaintOffset(size, trackWidth, trackHeight);
final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, visualPosition); 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); paintRadialReaction(canvas: canvas, origin: radialReactionOrigin);
_paintThumbWith( _paintThumbWith(
thumbPaintOffset, thumbPaintOffset,
...@@ -996,13 +1188,15 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -996,13 +1188,15 @@ class _SwitchPainter extends ToggleablePainter {
thumbColor, thumbColor,
thumbImage, thumbImage,
thumbErrorListener, thumbErrorListener,
thumbRadius,
thumbIcon,
); );
} }
/// Computes canvas offset for track's upper left corner /// Computes canvas offset for track's upper left corner
Offset _computeTrackPaintOffset(Size canvasSize, double trackWidth, double trackHeight) { Offset _computeTrackPaintOffset(Size canvasSize, double trackWidth, double trackHeight) {
final double horizontalOffset = (canvasSize.width - _kTrackWidth) / 2.0; final double horizontalOffset = (canvasSize.width - trackWidth) / 2.0;
final double verticalOffset = (canvasSize.height - _kTrackHeight) / 2.0; final double verticalOffset = (canvasSize.height - trackHeight) / 2.0;
return Offset(horizontalOffset, verticalOffset); return Offset(horizontalOffset, verticalOffset);
} }
...@@ -1011,7 +1205,9 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -1011,7 +1205,9 @@ class _SwitchPainter extends ToggleablePainter {
/// square /// square
Offset _computeThumbPaintOffset(Offset trackPaintOffset, double visualPosition) { Offset _computeThumbPaintOffset(Offset trackPaintOffset, double visualPosition) {
// How much thumb radius extends beyond the track // 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 horizontalProgress = visualPosition * trackInnerLength;
final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius + horizontalProgress; final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius + horizontalProgress;
...@@ -1020,19 +1216,39 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -1020,19 +1216,39 @@ class _SwitchPainter extends ToggleablePainter {
return Offset(thumbHorizontalOffset, thumbVerticalOffset); 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( final Rect trackRect = Rect.fromLTWH(
trackPaintOffset.dx, trackPaintOffset.dx,
trackPaintOffset.dy, trackPaintOffset.dy,
_kTrackWidth, trackWidth,
_kTrackHeight, trackHeight,
); );
final double trackRadius = trackHeight / 2;
final RRect trackRRect = RRect.fromRectAndRadius( final RRect trackRRect = RRect.fromRectAndRadius(
trackRect, trackRect,
const Radius.circular(_kTrackRadius), Radius.circular(trackRadius),
); );
canvas.drawRRect(trackRRect, paint); 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( void _paintThumbWith(
...@@ -1042,6 +1258,8 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -1042,6 +1258,8 @@ class _SwitchPainter extends ToggleablePainter {
Color thumbColor, Color thumbColor,
ImageProvider? thumbImage, ImageProvider? thumbImage,
ImageErrorListener? thumbErrorListener, ImageErrorListener? thumbErrorListener,
double thumbRadius,
Icon? thumbIcon,
) { ) {
try { try {
_isPainting = true; _isPainting = true;
...@@ -1056,13 +1274,51 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -1056,13 +1274,51 @@ class _SwitchPainter extends ToggleablePainter {
// The thumb contracts slightly during the animation // The thumb contracts slightly during the animation
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0; final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
final double radius = _kThumbRadius - inset; final double radius = thumbRadius - inset;
thumbPainter.paint( thumbPainter.paint(
canvas, canvas,
thumbPaintOffset + Offset(0, inset), thumbPaintOffset + Offset(0, inset),
configuration.copyWith(size: Size.fromRadius(radius)), 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 { } finally {
_isPainting = false; _isPainting = false;
} }
...@@ -1078,3 +1334,330 @@ class _SwitchPainter extends ToggleablePainter { ...@@ -1078,3 +1334,330 @@ class _SwitchPainter extends ToggleablePainter {
super.dispose(); 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 { ...@@ -43,6 +43,7 @@ class SwitchThemeData with Diagnosticable {
this.mouseCursor, this.mouseCursor,
this.overlayColor, this.overlayColor,
this.splashRadius, this.splashRadius,
this.thumbIcon,
}); });
/// {@macro flutter.material.switch.thumbColor} /// {@macro flutter.material.switch.thumbColor}
...@@ -76,6 +77,11 @@ class SwitchThemeData with Diagnosticable { ...@@ -76,6 +77,11 @@ class SwitchThemeData with Diagnosticable {
/// If specified, overrides the default value of [Switch.splashRadius]. /// If specified, overrides the default value of [Switch.splashRadius].
final double? 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 /// Creates a copy of this object but with the given fields replaced with the
/// new values. /// new values.
SwitchThemeData copyWith({ SwitchThemeData copyWith({
...@@ -85,6 +91,7 @@ class SwitchThemeData with Diagnosticable { ...@@ -85,6 +91,7 @@ class SwitchThemeData with Diagnosticable {
MaterialStateProperty<MouseCursor?>? mouseCursor, MaterialStateProperty<MouseCursor?>? mouseCursor,
MaterialStateProperty<Color?>? overlayColor, MaterialStateProperty<Color?>? overlayColor,
double? splashRadius, double? splashRadius,
MaterialStateProperty<Icon?>? thumbIcon,
}) { }) {
return SwitchThemeData( return SwitchThemeData(
thumbColor: thumbColor ?? this.thumbColor, thumbColor: thumbColor ?? this.thumbColor,
...@@ -93,6 +100,7 @@ class SwitchThemeData with Diagnosticable { ...@@ -93,6 +100,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor: mouseCursor ?? this.mouseCursor, mouseCursor: mouseCursor ?? this.mouseCursor,
overlayColor: overlayColor ?? this.overlayColor, overlayColor: overlayColor ?? this.overlayColor,
splashRadius: splashRadius ?? this.splashRadius, splashRadius: splashRadius ?? this.splashRadius,
thumbIcon: thumbIcon ?? this.thumbIcon,
); );
} }
...@@ -107,6 +115,7 @@ class SwitchThemeData with Diagnosticable { ...@@ -107,6 +115,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
overlayColor: MaterialStateProperty.lerp<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp), overlayColor: MaterialStateProperty.lerp<Color?>(a?.overlayColor, b?.overlayColor, t, Color.lerp),
splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t), splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t),
thumbIcon: t < 0.5 ? a?.thumbIcon : b?.thumbIcon,
); );
} }
...@@ -118,6 +127,7 @@ class SwitchThemeData with Diagnosticable { ...@@ -118,6 +127,7 @@ class SwitchThemeData with Diagnosticable {
mouseCursor, mouseCursor,
overlayColor, overlayColor,
splashRadius, splashRadius,
thumbIcon,
); );
@override @override
...@@ -134,7 +144,8 @@ class SwitchThemeData with Diagnosticable { ...@@ -134,7 +144,8 @@ class SwitchThemeData with Diagnosticable {
&& other.materialTapTargetSize == materialTapTargetSize && other.materialTapTargetSize == materialTapTargetSize
&& other.mouseCursor == mouseCursor && other.mouseCursor == mouseCursor
&& other.overlayColor == overlayColor && other.overlayColor == overlayColor
&& other.splashRadius == splashRadius; && other.splashRadius == splashRadius
&& other.thumbIcon == thumbIcon;
} }
@override @override
...@@ -146,6 +157,7 @@ class SwitchThemeData with Diagnosticable { ...@@ -146,6 +157,7 @@ class SwitchThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null)); properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
properties.add(DoubleProperty('splashRadius', splashRadius, 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 { ...@@ -1252,7 +1252,7 @@ class ThemeData with Diagnosticable {
/// * Typography: `typography` (see table above) /// * Typography: `typography` (see table above)
/// ///
/// ### Components /// ### Components
/// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton] /// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton], [IconButton]
/// * FAB: [FloatingActionButton] /// * FAB: [FloatingActionButton]
/// * Extended FAB: [FloatingActionButton.extended] /// * Extended FAB: [FloatingActionButton.extended]
/// * Cards: [Card] /// * Cards: [Card]
...@@ -1266,6 +1266,7 @@ class ThemeData with Diagnosticable { ...@@ -1266,6 +1266,7 @@ class ThemeData with Diagnosticable {
/// * Lists: [ListTile] /// * Lists: [ListTile]
/// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar]) /// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar])
/// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail] /// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail]
/// * Switch: [Switch]
/// * Top app bar: [AppBar] /// * Top app bar: [AppBar]
/// ///
/// In addition, this flag enables features introduced in Android 12. /// In addition, this flag enables features introduced in Android 12.
......
...@@ -21,6 +21,8 @@ import '../rendering/mock_canvas.dart'; ...@@ -21,6 +21,8 @@ import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
void main() { void main() {
final ThemeData theme = ThemeData();
testWidgets('Switch can toggle on tap', (WidgetTester tester) async { testWidgets('Switch can toggle on tap', (WidgetTester tester) async {
final Key switchKey = UniqueKey(); final Key switchKey = UniqueKey();
bool value = false; bool value = false;
...@@ -30,7 +32,9 @@ void main() { ...@@ -30,7 +32,9 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
return Material( return MaterialApp(
theme: theme,
home: Material(
child: Center( child: Center(
child: Switch( child: Switch(
dragStartBehavior: DragStartBehavior.down, dragStartBehavior: DragStartBehavior.down,
...@@ -43,6 +47,7 @@ void main() { ...@@ -43,6 +47,7 @@ void main() {
}, },
), ),
), ),
),
); );
}, },
), ),
...@@ -55,9 +60,10 @@ void main() { ...@@ -55,9 +60,10 @@ void main() {
}); });
testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async { testWidgets('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
final bool material3 = theme.useMaterial3;
await tester.pumpWidget( await tester.pumpWidget(
Theme( Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
...@@ -73,11 +79,14 @@ void main() { ...@@ -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( await tester.pumpWidget(
Theme( Theme(
data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Material( child: Material(
...@@ -93,10 +102,10 @@ void main() { ...@@ -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 maxWidth = 300;
const double maxHeight = 100; const double maxHeight = 100;
...@@ -151,7 +160,9 @@ void main() { ...@@ -151,7 +160,9 @@ void main() {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -171,6 +182,7 @@ void main() { ...@@ -171,6 +182,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, isFalse); expect(value, isFalse);
...@@ -198,7 +210,9 @@ void main() { ...@@ -198,7 +210,9 @@ void main() {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -218,6 +232,7 @@ void main() { ...@@ -218,6 +232,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, isFalse); expect(value, isFalse);
...@@ -287,7 +302,9 @@ void main() { ...@@ -287,7 +302,9 @@ void main() {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -307,6 +324,7 @@ void main() { ...@@ -307,6 +324,7 @@ void main() {
}, },
), ),
), ),
),
); );
await tester.drag(find.byType(Switch), const Offset(30.0, 0.0)); await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));
...@@ -513,7 +531,9 @@ void main() { ...@@ -513,7 +531,9 @@ void main() {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -533,6 +553,7 @@ void main() { ...@@ -533,6 +553,7 @@ void main() {
}, },
), ),
), ),
),
); );
expect(value, isFalse); expect(value, isFalse);
...@@ -554,7 +575,9 @@ void main() { ...@@ -554,7 +575,9 @@ void main() {
bool value = false; bool value = false;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -574,6 +597,7 @@ void main() { ...@@ -574,6 +597,7 @@ void main() {
}, },
), ),
), ),
),
); );
// Move a little to the right, not past the middle. // Move a little to the right, not past the middle.
...@@ -637,7 +661,9 @@ void main() { ...@@ -637,7 +661,9 @@ void main() {
final SemanticsTester semanticsTester = SemanticsTester(tester); final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -656,6 +682,7 @@ void main() { ...@@ -656,6 +682,7 @@ void main() {
}, },
), ),
), ),
),
); );
await tester.tap(find.byType(Switch)); await tester.tap(find.byType(Switch));
final RenderObject object = tester.firstRenderObject(find.byType(Switch)); final RenderObject object = tester.firstRenderObject(find.byType(Switch));
...@@ -682,6 +709,7 @@ void main() { ...@@ -682,6 +709,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: StatefulBuilder( home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
void onChanged(bool newValue) { void onChanged(bool newValue) {
...@@ -859,6 +887,7 @@ void main() { ...@@ -859,6 +887,7 @@ void main() {
const double splashRadius = 30; const double splashRadius = 30;
Widget buildApp() { Widget buildApp() {
return MaterialApp( return MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
...@@ -962,6 +991,7 @@ void main() { ...@@ -962,6 +991,7 @@ void main() {
bool value = true; bool value = true;
Widget buildApp({bool enabled = true}) { Widget buildApp({bool enabled = true}) {
return MaterialApp( return MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
...@@ -1001,6 +1031,7 @@ void main() { ...@@ -1001,6 +1031,7 @@ void main() {
// Test Switch.adaptive() constructor // Test Switch.adaptive() constructor
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: Align( body: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
...@@ -1029,6 +1060,7 @@ void main() { ...@@ -1029,6 +1060,7 @@ void main() {
// Test Switch() constructor // Test Switch() constructor
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: Align( body: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
...@@ -1053,6 +1085,7 @@ void main() { ...@@ -1053,6 +1085,7 @@ void main() {
// Test default cursor // Test default cursor
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: Align( body: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
...@@ -1074,8 +1107,9 @@ void main() { ...@@ -1074,8 +1107,9 @@ void main() {
// Test default cursor when disabled // Test default cursor when disabled
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( MaterialApp(
home: Scaffold( theme: theme,
home: const Scaffold(
body: Align( body: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Material( child: Material(
...@@ -1103,7 +1137,9 @@ void main() { ...@@ -1103,7 +1137,9 @@ void main() {
bool enabled = true; bool enabled = true;
late StateSetter stateSetter; late StateSetter stateSetter;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Theme(
data: theme,
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: StatefulBuilder( child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) { builder: (BuildContext context, StateSetter setState) {
...@@ -1123,6 +1159,7 @@ void main() { ...@@ -1123,6 +1159,7 @@ void main() {
}, },
), ),
), ),
),
); );
final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch')); final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));
...@@ -1580,6 +1617,7 @@ void main() { ...@@ -1580,6 +1617,7 @@ void main() {
Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) { Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) {
return MaterialApp( return MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: Switch( body: Switch(
focusNode: focusNode, focusNode: focusNode,
...@@ -1701,6 +1739,7 @@ void main() { ...@@ -1701,6 +1739,7 @@ void main() {
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async { testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
Widget buildSwitch(bool show) { Widget buildSwitch(bool show) {
return MaterialApp( return MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: show ? Switch(value: true, onChanged: (_) { }) : Container(), child: show ? Switch(value: true, onChanged: (_) { }) : Container(),
...@@ -1785,11 +1824,53 @@ void main() { ...@@ -1785,11 +1824,53 @@ void main() {
image = await createTestImage(width: 100, height: 100); 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 { testWidgets('do not crash when imageProvider completes after Switch is disposed', (WidgetTester tester) async {
final DelayedImageProvider imageProvider = DelayedImageProvider(image); final DelayedImageProvider imageProvider = DelayedImageProvider(image);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: Switch( child: Switch(
...@@ -1819,6 +1900,7 @@ void main() { ...@@ -1819,6 +1900,7 @@ void main() {
Future<void> buildSwitch(ImageProvider imageProvider) { Future<void> buildSwitch(ImageProvider imageProvider) {
return tester.pumpWidget( return tester.pumpWidget(
MaterialApp( MaterialApp(
theme: theme,
home: Material( home: Material(
child: Center( child: Center(
child: Switch( child: Switch(
...@@ -1850,27 +1932,933 @@ void main() { ...@@ -1850,27 +1932,933 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
}); });
}
class DelayedImageProvider extends ImageProvider<DelayedImageProvider> { group('Switch M3 tests', () {
DelayedImageProvider(this.image); 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 testWidgets('Inactive Switch has default colors when disabled - M3', (WidgetTester tester) async {
Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) { final ThemeData themeData = ThemeData(useMaterial3: true, colorSchemeSeed: const Color(0xff6750a4), brightness: Brightness.light);
return SynchronousFuture<DelayedImageProvider>(this); final ColorScheme colors = themeData.colorScheme;
}
@override await tester.pumpWidget(MaterialApp(
ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) { theme: themeData,
return OneFrameImageStreamCompleter(_completer.future); 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 { // active, enabled switch
_completer.complete(ImageInfo(image: image)); 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 @override
......
...@@ -23,6 +23,7 @@ void main() { ...@@ -23,6 +23,7 @@ void main() {
expect(themeData.materialTapTargetSize, null); expect(themeData.materialTapTargetSize, null);
expect(themeData.overlayColor, null); expect(themeData.overlayColor, null);
expect(themeData.splashRadius, null); expect(themeData.splashRadius, null);
expect(themeData.thumbIcon, null);
const SwitchTheme theme = SwitchTheme(data: SwitchThemeData(), child: SizedBox()); const SwitchTheme theme = SwitchTheme(data: SwitchThemeData(), child: SizedBox());
expect(theme.data.thumbColor, null); expect(theme.data.thumbColor, null);
...@@ -31,6 +32,7 @@ void main() { ...@@ -31,6 +32,7 @@ void main() {
expect(theme.data.materialTapTargetSize, null); expect(theme.data.materialTapTargetSize, null);
expect(theme.data.overlayColor, null); expect(theme.data.overlayColor, null);
expect(theme.data.splashRadius, null); expect(theme.data.splashRadius, null);
expect(theme.data.thumbIcon, null);
}); });
testWidgets('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -54,6 +56,7 @@ void main() { ...@@ -54,6 +56,7 @@ void main() {
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
overlayColor: MaterialStatePropertyAll<Color>(Color(0xfffffff2)), overlayColor: MaterialStatePropertyAll<Color>(Color(0xfffffff2)),
splashRadius: 1.0, splashRadius: 1.0,
thumbIcon: MaterialStatePropertyAll<Icon>(Icon(IconData(123))),
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -67,6 +70,7 @@ void main() { ...@@ -67,6 +70,7 @@ void main() {
expect(description[3], 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))'); expect(description[3], 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))');
expect(description[4], 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff2))'); expect(description[4], 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff2))');
expect(description[5], 'splashRadius: 1.0'); expect(description[5], 'splashRadius: 1.0');
expect(description[6], 'thumbIcon: MaterialStatePropertyAll(Icon(IconData(U+0007B)))');
}); });
testWidgets('Switch is themeable', (WidgetTester tester) async { testWidgets('Switch is themeable', (WidgetTester tester) async {
...@@ -81,10 +85,11 @@ void main() { ...@@ -81,10 +85,11 @@ void main() {
const Color focusOverlayColor = Color(0xfffffff4); const Color focusOverlayColor = Color(0xfffffff4);
const Color hoverOverlayColor = Color(0xfffffff5); const Color hoverOverlayColor = Color(0xfffffff5);
const double splashRadius = 1.0; 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}) { final ThemeData themeData = ThemeData(
return MaterialApp( useMaterial3: true,
theme: ThemeData(
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) { if (states.contains(MaterialState.selected)) {
...@@ -110,8 +115,18 @@ void main() { ...@@ -110,8 +115,18 @@ void main() {
return null; return null;
}), }),
splashRadius: splashRadius, 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( home: Scaffold(
body: Switch( body: Switch(
dragStartBehavior: DragStartBehavior.down, dragStartBehavior: DragStartBehavior.down,
...@@ -128,27 +143,39 @@ void main() { ...@@ -128,27 +143,39 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor)
..paragraph()
)
: (paints
..rrect(color: defaultTrackColor) ..rrect(color: defaultTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: defaultThumbColor), ..circle(color: defaultThumbColor)
)
); );
// Size from MaterialTapTargetSize.shrinkWrap. // 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. // Selected switch.
await tester.pumpWidget(buildSwitch(selected: true)); await tester.pumpWidget(buildSwitch(selected: true));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor)..paragraph())
: (paints
..rrect(color: selectedTrackColor) ..rrect(color: selectedTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: selectedThumbColor), ..circle(color: selectedThumbColor))
); );
// Switch with hover. // Switch with hover.
...@@ -187,9 +214,7 @@ void main() { ...@@ -187,9 +214,7 @@ void main() {
const Color hoverColor = Color(0xffffff5f); const Color hoverColor = Color(0xffffff5f);
const double splashRadius = 2.0; const double splashRadius = 2.0;
Widget buildSwitch({bool selected = false, bool autofocus = false}) { final ThemeData themeData = ThemeData(
return MaterialApp(
theme: ThemeData(
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) { if (states.contains(MaterialState.selected)) {
...@@ -215,8 +240,19 @@ void main() { ...@@ -215,8 +240,19 @@ void main() {
return null; return null;
}), }),
splashRadius: themeSplashRadius, 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( home: Scaffold(
body: Switch( body: Switch(
value: selected, value: selected,
...@@ -239,6 +275,12 @@ void main() { ...@@ -239,6 +275,12 @@ void main() {
focusColor: focusColor, focusColor: focusColor,
hoverColor: hoverColor, hoverColor: hoverColor,
splashRadius: splashRadius, 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() { ...@@ -249,27 +291,36 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _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) ..rrect(color: defaultTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: defaultThumbColor), ..circle(color: defaultThumbColor))
); );
// Size from MaterialTapTargetSize.shrinkWrap. // 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. // Selected switch.
await tester.pumpWidget(buildSwitch(selected: true)); await tester.pumpWidget(buildSwitch(selected: true));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor) ..rrect(color: selectedTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: selectedThumbColor), ..circle(color: selectedThumbColor))
); );
// Switch with hover. // Switch with hover.
...@@ -298,9 +349,7 @@ void main() { ...@@ -298,9 +349,7 @@ void main() {
const Color defaultTrackColor = Color(0xffffff2f); const Color defaultTrackColor = Color(0xffffff2f);
const Color selectedTrackColor = Color(0xffffff3f); const Color selectedTrackColor = Color(0xffffff3f);
Widget buildSwitch({bool selected = false, bool autofocus = false}) { final ThemeData themeData = ThemeData(
return MaterialApp(
theme: ThemeData(
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { thumbColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) { if (states.contains(MaterialState.selected)) {
...@@ -315,7 +364,12 @@ void main() { ...@@ -315,7 +364,12 @@ void main() {
return themeDefaultTrackColor; return themeDefaultTrackColor;
}), }),
), ),
), );
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold( home: Scaffold(
body: Switch( body: Switch(
value: selected, value: selected,
...@@ -335,12 +389,17 @@ void main() { ...@@ -335,12 +389,17 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor))
: (paints
..rrect(color: defaultTrackColor) ..rrect(color: defaultTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: defaultThumbColor), ..circle(color: defaultThumbColor))
); );
// Selected switch. // Selected switch.
...@@ -348,12 +407,16 @@ void main() { ...@@ -348,12 +407,16 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor) ..rrect(color: selectedTrackColor)
..circle() ..circle()
..circle() ..circle()
..circle() ..circle()
..circle(color: selectedThumbColor), ..circle(color: selectedThumbColor))
); );
}); });
...@@ -371,15 +434,17 @@ void main() { ...@@ -371,15 +434,17 @@ void main() {
return null; return null;
} }
const double splashRadius = 24.0; const double splashRadius = 24.0;
final ThemeData themeData = ThemeData(
Widget buildSwitch({required bool active}) {
return MaterialApp(
theme: ThemeData(
switchTheme: SwitchThemeData( switchTheme: SwitchThemeData(
overlayColor: MaterialStateProperty.resolveWith(getOverlayColor), overlayColor: MaterialStateProperty.resolveWith(getOverlayColor),
splashRadius: splashRadius, splashRadius: splashRadius,
), ),
), );
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({required bool active}) {
return MaterialApp(
theme: themeData,
home: Scaffold( home: Scaffold(
body: Switch( body: Switch(
value: active, value: active,
...@@ -395,12 +460,20 @@ void main() { ...@@ -395,12 +460,20 @@ void main() {
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? ((paints
..rrect() ..rrect()
..rrect())
..circle( ..circle(
color: inactivePressedOverlayColor, color: inactivePressedOverlayColor,
radius: splashRadius, radius: splashRadius,
), ))
: (paints
..rrect()
..circle(
color: inactivePressedOverlayColor,
radius: splashRadius,
)),
reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor', reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor',
); );
...@@ -426,14 +499,16 @@ void main() { ...@@ -426,14 +499,16 @@ void main() {
const Color localThemeThumbColor = Color(0xffff0000); const Color localThemeThumbColor = Color(0xffff0000);
const Color localThemeTrackColor = Color(0xffff0000); const Color localThemeTrackColor = Color(0xffff0000);
Widget buildSwitch({bool selected = false, bool autofocus = false}) { final ThemeData themeData = ThemeData(
return MaterialApp(
theme: ThemeData(
switchTheme: const SwitchThemeData( switchTheme: const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(globalThemeThumbColor), thumbColor: MaterialStatePropertyAll<Color>(globalThemeThumbColor),
trackColor: MaterialStatePropertyAll<Color>(globalThemeTrackColor), trackColor: MaterialStatePropertyAll<Color>(globalThemeTrackColor),
), ),
), );
final bool material3 = themeData.useMaterial3;
Widget buildSwitch({bool selected = false, bool autofocus = false}) {
return MaterialApp(
theme: themeData,
home: Scaffold( home: Scaffold(
body: SwitchTheme( body: SwitchTheme(
data: const SwitchThemeData( data: const SwitchThemeData(
...@@ -454,12 +529,16 @@ void main() { ...@@ -454,12 +529,16 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
_getSwitchMaterial(tester), _getSwitchMaterial(tester),
paints material3
? (paints
..rrect(color: localThemeTrackColor)
..circle(color: localThemeThumbColor))
: (paints
..rrect(color: localThemeTrackColor) ..rrect(color: localThemeTrackColor)
..circle() ..circle()
..circle() ..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