diff --git a/dev/tools/gen_defaults/lib/switch_template.dart b/dev/tools/gen_defaults/lib/switch_template.dart index 5592c2039a3..05c1a597470 100644 --- a/dev/tools/gen_defaults/lib/switch_template.dart +++ b/dev/tools/gen_defaults/lib/switch_template.dart @@ -137,6 +137,9 @@ class _${blockName}DefaultsM3 extends SwitchThemeData { @override double get splashRadius => ${getToken('md.comp.switch.state-layer.size')} / 2; + + @override + EdgeInsetsGeometry? get padding => const EdgeInsets.symmetric(horizontal: 4); } class _SwitchConfigM3 with _SwitchConfig { @@ -192,13 +195,13 @@ class _SwitchConfigM3 with _SwitchConfig { double get pressedThumbRadius => ${getToken('md.comp.switch.pressed.handle.width')} / 2; @override - double get switchHeight => _kSwitchMinSize + 8.0; + double get switchHeight => switchMinSize.height + 8.0; @override - double get switchHeightCollapsed => _kSwitchMinSize; + double get switchHeightCollapsed => switchMinSize.height; @override - double get switchWidth => trackWidth - 2 * (trackHeight / 2.0) + _kSwitchMinSize; + double get switchWidth => 52.0; @override double get thumbRadiusWithIcon => ${getToken('md.comp.switch.with-icon.handle.width')} / 2; @@ -223,6 +226,9 @@ class _SwitchConfigM3 with _SwitchConfig { // Hand coded default based on the animation specs. @override double? get thumbOffset => null; + + @override + Size get switchMinSize => const Size(kMinInteractiveDimension, kMinInteractiveDimension - 8.0); } '''; diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index e08aa3bc299..034e7e8f8e9 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -23,8 +23,6 @@ import 'theme_data.dart'; // bool _giveVerse = true; // late StateSetter setState; -const double _kSwitchMinSize = kMinInteractiveDimension - 8.0; - enum _SwitchType { material, adaptive } /// A Material Design switch. @@ -124,6 +122,7 @@ class Switch extends StatelessWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.padding, }) : _switchType = _SwitchType.material, applyCupertinoTheme = false, assert(activeThumbImage != null || onActiveThumbImageError == null), @@ -177,6 +176,7 @@ class Switch extends StatelessWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.padding, this.applyCupertinoTheme, }) : assert(activeThumbImage != null || onActiveThumbImageError == null), assert(inactiveThumbImage != null || onInactiveThumbImageError == null), @@ -552,9 +552,16 @@ class Switch extends StatelessWidget { /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; + /// The amount of space to surround the child inside the bounds of the [Switch]. + /// + /// Defaults to horizontal padding of 4 pixels. If [ThemeData.useMaterial3] is false, + /// then there is no padding by default. + final EdgeInsetsGeometry? padding; + Size _getSwitchSize(BuildContext context) { final ThemeData theme = Theme.of(context); SwitchThemeData switchTheme = SwitchTheme.of(context); + final SwitchThemeData defaults = theme.useMaterial3 ? _SwitchDefaultsM3(context) : _SwitchDefaultsM2(context); if (_switchType == _SwitchType.adaptive) { final Adaptation switchAdaptation = theme.getAdaptation() ?? const _SwitchThemeAdaptation(); @@ -565,9 +572,18 @@ class Switch extends StatelessWidget { final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize ?? switchTheme.materialTapTargetSize ?? theme.materialTapTargetSize; + final EdgeInsetsGeometry effectivePadding = padding + ?? switchTheme.padding + ?? defaults.padding!; return switch (effectiveMaterialTapTargetSize) { - MaterialTapTargetSize.padded => Size(switchConfig.switchWidth, switchConfig.switchHeight), - MaterialTapTargetSize.shrinkWrap => Size(switchConfig.switchWidth, switchConfig.switchHeightCollapsed), + MaterialTapTargetSize.padded => Size( + switchConfig.switchWidth + effectivePadding.horizontal, + switchConfig.switchHeight + effectivePadding.vertical, + ), + MaterialTapTargetSize.shrinkWrap => Size( + switchConfig.switchWidth + effectivePadding.horizontal, + switchConfig.switchHeightCollapsed + effectivePadding.vertical, + ), }; } @@ -789,7 +805,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta case TargetPlatform.fuchsia: case TargetPlatform.linux: case TargetPlatform.windows: - return widget.size.width - _kSwitchMinSize; + final _SwitchConfig config = Theme.of(context).useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2(); + final double trackInnerStart = config.trackHeight / 2.0; + final double trackInnerEnd = config.trackWidth - trackInnerStart; + final double trackInnerLength = trackInnerEnd - trackInnerStart; + return trackInnerLength; case TargetPlatform.iOS: case TargetPlatform.macOS: final _SwitchConfig config = _SwitchConfigCupertino(context); @@ -799,7 +819,11 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta return trackInnerLength; } case _SwitchType.material: - return widget.size.width - _kSwitchMinSize; + final _SwitchConfig config = Theme.of(context).useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2(); + final double trackInnerStart = config.trackHeight / 2.0; + final double trackInnerEnd = config.trackWidth - trackInnerStart; + final double trackInnerLength = trackInnerEnd - trackInnerStart; + return trackInnerLength; } } @@ -1782,6 +1806,7 @@ mixin _SwitchConfig { double? get thumbOffset; Size get transitionalThumbSize; int get toggleDuration; + Size get switchMinSize; } // Hand coded defaults for iOS/macOS Switch @@ -1862,10 +1887,10 @@ class _SwitchConfigCupertino with _SwitchConfig { double get pressedThumbRadius => 14.0; @override - double get switchHeight => _kSwitchMinSize + 8.0; + double get switchHeight => switchMinSize.height + 8.0; @override - double get switchHeightCollapsed => _kSwitchMinSize; + double get switchHeightCollapsed => switchMinSize.height; @override double get switchWidth => 60.0; @@ -1904,6 +1929,9 @@ class _SwitchConfigCupertino with _SwitchConfig { // Hand coded default based on the animation specs. @override double? get thumbOffset => null; + + @override + Size get switchMinSize => const Size.square(kMinInteractiveDimension - 8.0); } // Hand coded defaults based on Material Design 2. @@ -1923,13 +1951,13 @@ class _SwitchConfigM2 with _SwitchConfig { double get pressedThumbRadius => 10.0; @override - double get switchHeight => _kSwitchMinSize + 8.0; + double get switchHeight => switchMinSize.height + 8.0; @override - double get switchHeightCollapsed => _kSwitchMinSize; + double get switchHeightCollapsed => switchMinSize.height; @override - double get switchWidth => trackWidth - 2 * (trackHeight / 2.0) + _kSwitchMinSize; + double get switchWidth => trackWidth - 2 * (trackHeight / 2.0) + switchMinSize.width; @override double get thumbRadiusWithIcon => 10.0; @@ -1951,6 +1979,9 @@ class _SwitchConfigM2 with _SwitchConfig { @override int get toggleDuration => 200; + + @override + Size get switchMinSize => const Size.square(kMinInteractiveDimension - 8.0); } class _SwitchDefaultsM2 extends SwitchThemeData { @@ -2021,6 +2052,9 @@ class _SwitchDefaultsM2 extends SwitchThemeData { @override double get splashRadius => kRadialReactionRadius; + + @override + EdgeInsetsGeometry? get padding => EdgeInsets.zero; } // BEGIN GENERATED TOKEN PROPERTIES - Switch @@ -2156,6 +2190,9 @@ class _SwitchDefaultsM3 extends SwitchThemeData { @override double get splashRadius => 40.0 / 2; + + @override + EdgeInsetsGeometry? get padding => const EdgeInsets.symmetric(horizontal: 4); } class _SwitchConfigM3 with _SwitchConfig { @@ -2211,13 +2248,13 @@ class _SwitchConfigM3 with _SwitchConfig { double get pressedThumbRadius => 28.0 / 2; @override - double get switchHeight => _kSwitchMinSize + 8.0; + double get switchHeight => switchMinSize.height + 8.0; @override - double get switchHeightCollapsed => _kSwitchMinSize; + double get switchHeightCollapsed => switchMinSize.height; @override - double get switchWidth => trackWidth - 2 * (trackHeight / 2.0) + _kSwitchMinSize; + double get switchWidth => 52.0; @override double get thumbRadiusWithIcon => 24.0 / 2; @@ -2242,6 +2279,9 @@ class _SwitchConfigM3 with _SwitchConfig { // Hand coded default based on the animation specs. @override double? get thumbOffset => null; + + @override + Size get switchMinSize => const Size(kMinInteractiveDimension, kMinInteractiveDimension - 8.0); } // END GENERATED TOKEN PROPERTIES - Switch diff --git a/packages/flutter/lib/src/material/switch_theme.dart b/packages/flutter/lib/src/material/switch_theme.dart index e0753cf8842..81253209523 100644 --- a/packages/flutter/lib/src/material/switch_theme.dart +++ b/packages/flutter/lib/src/material/switch_theme.dart @@ -46,6 +46,7 @@ class SwitchThemeData with Diagnosticable { this.overlayColor, this.splashRadius, this.thumbIcon, + this.padding, }); /// {@macro flutter.material.switch.thumbColor} @@ -94,6 +95,9 @@ class SwitchThemeData with Diagnosticable { /// It is overridden by [Switch.thumbIcon]. final MaterialStateProperty? thumbIcon; + /// If specified, overrides the default value of [Switch.padding]. + final EdgeInsetsGeometry? padding; + /// Creates a copy of this object but with the given fields replaced with the /// new values. SwitchThemeData copyWith({ @@ -106,6 +110,7 @@ class SwitchThemeData with Diagnosticable { MaterialStateProperty? overlayColor, double? splashRadius, MaterialStateProperty? thumbIcon, + EdgeInsetsGeometry? padding, }) { return SwitchThemeData( thumbColor: thumbColor ?? this.thumbColor, @@ -117,6 +122,7 @@ class SwitchThemeData with Diagnosticable { overlayColor: overlayColor ?? this.overlayColor, splashRadius: splashRadius ?? this.splashRadius, thumbIcon: thumbIcon ?? this.thumbIcon, + padding: padding ?? this.padding, ); } @@ -137,6 +143,7 @@ class SwitchThemeData with Diagnosticable { overlayColor: MaterialStateProperty.lerp(a?.overlayColor, b?.overlayColor, t, Color.lerp), splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t), thumbIcon: t < 0.5 ? a?.thumbIcon : b?.thumbIcon, + padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), ); } @@ -151,6 +158,7 @@ class SwitchThemeData with Diagnosticable { overlayColor, splashRadius, thumbIcon, + padding, ); @override @@ -170,7 +178,8 @@ class SwitchThemeData with Diagnosticable { && other.mouseCursor == mouseCursor && other.overlayColor == overlayColor && other.splashRadius == splashRadius - && other.thumbIcon == thumbIcon; + && other.thumbIcon == thumbIcon + && other.padding == padding; } @override @@ -185,6 +194,7 @@ class SwitchThemeData with Diagnosticable { properties.add(DiagnosticsProperty>('overlayColor', overlayColor, defaultValue: null)); properties.add(DoubleProperty('splashRadius', splashRadius, defaultValue: null)); properties.add(DiagnosticsProperty>('thumbIcon', thumbIcon, defaultValue: null)); + properties.add(DiagnosticsProperty('padding', padding, defaultValue: null)); } } diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index e37e74a9ebd..b7989e674bf 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -4091,6 +4091,34 @@ void main() { focusNode.dispose(); }); + + testWidgets('Switch.padding is respected', (WidgetTester tester) async { + Widget buildSwitch({ EdgeInsets? padding }) { + return MaterialApp( + home: Material( + child: Center( + child: Switch( + padding: padding, + value: true, + onChanged: (_) {}, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildSwitch()); + + expect(tester.getSize(find.byType(Switch)), const Size(60.0, 48.0)); + + await tester.pumpWidget(buildSwitch(padding: EdgeInsets.zero)); + + expect(tester.getSize(find.byType(Switch)), const Size(52.0, 48.0)); + + await tester.pumpWidget(buildSwitch(padding: const EdgeInsets.all(4.0))); + + expect(tester.getSize(find.byType(Switch)), const Size(60.0, 56.0)); + }); } class DelayedImageProvider extends ImageProvider { diff --git a/packages/flutter/test/material/switch_theme_test.dart b/packages/flutter/test/material/switch_theme_test.dart index 8b9d8b17d1c..3469689386c 100644 --- a/packages/flutter/test/material/switch_theme_test.dart +++ b/packages/flutter/test/material/switch_theme_test.dart @@ -29,6 +29,7 @@ void main() { expect(themeData.overlayColor, null); expect(themeData.splashRadius, null); expect(themeData.thumbIcon, null); + expect(themeData.padding, null); const SwitchTheme theme = SwitchTheme(data: SwitchThemeData(), child: SizedBox()); expect(theme.data.thumbColor, null); @@ -40,6 +41,7 @@ void main() { expect(theme.data.overlayColor, null); expect(theme.data.splashRadius, null); expect(theme.data.thumbIcon, null); + expect(theme.data.padding, null); }); testWidgets('Default SwitchThemeData debugFillProperties', (WidgetTester tester) async { @@ -66,6 +68,7 @@ void main() { overlayColor: MaterialStatePropertyAll(Color(0xfffffff2)), splashRadius: 1.0, thumbIcon: MaterialStatePropertyAll(Icon(IconData(123))), + padding: EdgeInsets.all(4.0), ).debugFillProperties(builder); final List description = builder.properties @@ -82,6 +85,7 @@ void main() { expect(description[6], 'overlayColor: WidgetStatePropertyAll(Color(0xfffffff2))'); expect(description[7], 'splashRadius: 1.0'); expect(description[8], 'thumbIcon: WidgetStatePropertyAll(Icon(IconData(U+0007B)))'); + expect(description[9], 'padding: EdgeInsets.all(4.0)'); }); testWidgets('Material2 - Switch is themeable', (WidgetTester tester) async { @@ -1041,6 +1045,40 @@ void main() { ..rrect(color: localThemeThumbColor) ); }); + + testWidgets('SwitchTheme padding is respected', (WidgetTester tester) async { + Widget buildSwitch({ EdgeInsets? padding }) { + return MaterialApp( + theme: ThemeData( + switchTheme: SwitchThemeData( + padding: padding, + ), + ), + home: Scaffold( + body: Center( + child: Switch( + value: true, + onChanged: (_) {}, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildSwitch()); + + expect(tester.getSize(find.byType(Switch)), const Size(60.0, 48.0)); + + await tester.pumpWidget(buildSwitch(padding: EdgeInsets.zero)); + await tester.pumpAndSettle(); + + expect(tester.getSize(find.byType(Switch)), const Size(52.0, 48.0)); + + await tester.pumpWidget(buildSwitch(padding: const EdgeInsets.all(4.0))); + await tester.pumpAndSettle(); + + expect(tester.getSize(find.byType(Switch)), const Size(60.0, 56.0)); + }); } Future _pointGestureToSwitch(WidgetTester tester) async {