Introduce Switch.padding (#149884)

fixes [Switch has some padding that leads to uncentered UI](https://github.com/flutter/flutter/issues/148498)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              ColoredBox(
                color: Colors.amber,
                child: Switch(
                  padding: EdgeInsets.zero,
                  value: true,
                  materialTapTargetSize: MaterialTapTargetSize.padded,
                  onChanged: (bool value) {},
                ),
              ),
              const SizedBox(height: 16),
              ColoredBox(
                color: Colors.amber,
                child: Switch(
                  value: true,
                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  onChanged: (bool value) {},
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

</details>

### Default Switch size

<img width="476" alt="Screenshot 2024-07-11 at 13 25 05" src="https://github.com/flutter/flutter/assets/48603081/f9f3f6c6-443d-4bd5-81d4-5e314554b032">

### Update Switch size using the new `Switch.padding` to address  [Switch has some padding that leads to uncentered UI](https://github.com/flutter/flutter/issues/148498)

<img width="476" alt="Screenshot 2024-07-11 at 13 24 40" src="https://github.com/flutter/flutter/assets/48603081/aea0717b-e852-4b8d-b703-c8c4999d4863">
This commit is contained in:
Taha Tesser 2024-07-16 23:25:09 +03:00 committed by GitHub
parent 22a5c6cb0a
commit e1cd7b11f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 18 deletions

View File

@ -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);
}
''';

View File

@ -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<SwitchThemeData> switchAdaptation = theme.getAdaptation<SwitchThemeData>()
?? 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

View File

@ -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<Icon?>? 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<Color?>? overlayColor,
double? splashRadius,
MaterialStateProperty<Icon?>? 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<Color?>(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<MaterialStateProperty<Color?>>('overlayColor', overlayColor, defaultValue: null));
properties.add(DoubleProperty('splashRadius', splashRadius, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Icon?>>('thumbIcon', thumbIcon, defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
}
}

View File

@ -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<DelayedImageProvider> {

View File

@ -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>(Color(0xfffffff2)),
splashRadius: 1.0,
thumbIcon: MaterialStatePropertyAll<Icon>(Icon(IconData(123))),
padding: EdgeInsets.all(4.0),
).debugFillProperties(builder);
final List<String> 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<void> _pointGestureToSwitch(WidgetTester tester) async {