diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 7f3981e8e9..5c3ce918d0 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -620,9 +620,15 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta // During a drag we may have modified the curve, reset it if its possible // to do without visual discontinuation. if (position.value == 0.0 || position.value == 1.0) { - position - ..curve = Curves.easeIn - ..reverseCurve = Curves.easeOut; + if (Theme.of(context).useMaterial3) { + position + ..curve = Curves.easeOutBack + ..reverseCurve = Curves.easeOutBack.flipped; + } else { + position + ..curve = Curves.easeIn + ..reverseCurve = Curves.easeOut; + } } animateToValue(); } @@ -693,7 +699,7 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta void _handleDragEnd(DragEndDetails details) { if (position.value >= 0.5 != widget.value) { - widget.onChanged!(!widget.value); + widget.onChanged?.call(!widget.value); // Wait with finishing the animation until widget.value has changed to // !widget.value as part of the widget.onChanged call above. setState(() { @@ -709,7 +715,7 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta void _handleChanged(bool? value) { assert(value != null); assert(widget.onChanged != null); - widget.onChanged!(value!); + widget.onChanged?.call(value!); } @override @@ -727,11 +733,6 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta final SwitchThemeData defaults = theme.useMaterial3 ? _SwitchDefaultsM3(context) : _SwitchDefaultsM2(context); positionController.duration = Duration(milliseconds: switchConfig.toggleDuration); - if (theme.useMaterial3) { - position - ..curve = Curves.easeOutBack - ..reverseCurve = Curves.easeOutBack.flipped; - } // Colors need to be resolved in selected and non selected states separately // so that they can be lerped between. @@ -780,12 +781,20 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ?? defaults.overlayColor!.resolve(hoveredStates)!; final Set activePressedStates = activeStates..add(MaterialState.pressed); + final Color effectiveActivePressedThumbColor = widget.thumbColor?.resolve(activePressedStates) + ?? _widgetThumbColor.resolve(activePressedStates) + ?? switchTheme.thumbColor?.resolve(activePressedStates) + ?? defaults.thumbColor!.resolve(activePressedStates)!; final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) ?? switchTheme.overlayColor?.resolve(activePressedStates) ?? activeThumbColor?.withAlpha(kRadialReactionAlpha) ?? defaults.overlayColor!.resolve(activePressedStates)!; final Set inactivePressedStates = inactiveStates..add(MaterialState.pressed); + final Color effectiveInactivePressedThumbColor = widget.thumbColor?.resolve(inactivePressedStates) + ?? _widgetThumbColor.resolve(inactivePressedStates) + ?? switchTheme.thumbColor?.resolve(inactivePressedStates) + ?? defaults.thumbColor!.resolve(inactivePressedStates)!; final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) ?? switchTheme.overlayColor?.resolve(inactivePressedStates) ?? inactiveThumbColor?.withAlpha(kRadialReactionAlpha) @@ -830,6 +839,8 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta ..isHovered = states.contains(MaterialState.hovered) ..activeColor = effectiveActiveThumbColor ..inactiveColor = effectiveInactiveThumbColor + ..activePressedColor = effectiveActivePressedThumbColor + ..inactivePressedColor = effectiveInactivePressedThumbColor ..activeThumbImage = widget.activeThumbImage ..onActiveThumbImageError = widget.onActiveThumbImageError ..inactiveThumbImage = widget.inactiveThumbImage @@ -926,6 +937,28 @@ class _SwitchPainter extends ToggleablePainter { notifyListeners(); } + Color get activePressedColor => _activePressedColor!; + Color? _activePressedColor; + set activePressedColor(Color? value) { + assert(value != null); + if (value == _activePressedColor) { + return; + } + _activePressedColor = value; + notifyListeners(); + } + + Color get inactivePressedColor => _inactivePressedColor!; + Color? _inactivePressedColor; + set inactivePressedColor(Color? value) { + assert(value != null); + if (value == _inactivePressedColor) { + return; + } + _inactivePressedColor = value; + notifyListeners(); + } + double get activeThumbRadius => _activeThumbRadius!; double? _activeThumbRadius; set activeThumbRadius(double value) { @@ -1180,7 +1213,7 @@ class _SwitchPainter extends ToggleablePainter { visualPosition = currentValue; break; } - if (reaction.status == AnimationStatus.reverse && _stopPressAnimation == false) { + if (reaction.status == AnimationStatus.reverse && !_stopPressAnimation) { _stopPressAnimation = true; } else { _stopPressAnimation = false; @@ -1189,7 +1222,7 @@ class _SwitchPainter extends ToggleablePainter { // To get the thumb radius when the press ends, the value can be any number // between activeThumbRadius/inactiveThumbRadius and pressedThumbRadius. if (!_stopPressAnimation) { - if (reaction.status == AnimationStatus.completed) { + if (reaction.isCompleted) { // This happens when the thumb is dragged instead of being tapped. _pressedInactiveThumbRadius = lerpDouble(inactiveThumbRadius, pressedThumbRadius, reaction.value); _pressedActiveThumbRadius = lerpDouble(activeThumbRadius, pressedThumbRadius, reaction.value); @@ -1248,10 +1281,10 @@ class _SwitchPainter extends ToggleablePainter { } Size thumbSize; - if (reaction.status == AnimationStatus.completed) { + if (reaction.isCompleted) { thumbSize = Size.fromRadius(pressedThumbRadius); } else { - if (position.status == AnimationStatus.dismissed || position.status == AnimationStatus.forward) { + if (position.isDismissed || position.status == AnimationStatus.forward) { thumbSize = thumbSizeAnimation(true).value; } else { thumbSize = thumbSizeAnimation(false).value; @@ -1262,10 +1295,21 @@ class _SwitchPainter extends ToggleablePainter { final double inset = thumbOffset == null ? 0 : 1.0 - (currentValue - thumbOffset!).abs() * 2.0; thumbSize = Size(thumbSize.width - inset, thumbSize.height - inset); - final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!; + final double colorValue = CurvedAnimation(parent: positionController, curve: Curves.easeOut, reverseCurve: Curves.easeIn).value; + final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, colorValue)!; final Color? trackOutlineColor = inactiveTrackOutlineColor == null ? null - : Color.lerp(inactiveTrackOutlineColor, Colors.transparent, currentValue); - final Color lerpedThumbColor = Color.lerp(inactiveColor, activeColor, currentValue)!; + : Color.lerp(inactiveTrackOutlineColor, Colors.transparent, colorValue); + Color lerpedThumbColor; + if (!reaction.isDismissed) { + lerpedThumbColor = Color.lerp(inactivePressedColor, activePressedColor, colorValue)!; + } else if (positionController.status == AnimationStatus.forward) { + lerpedThumbColor = Color.lerp(inactivePressedColor, activeColor, colorValue)!; + } else if (positionController.status == AnimationStatus.reverse) { + lerpedThumbColor = Color.lerp(inactiveColor, activePressedColor, colorValue)!; + } else { + lerpedThumbColor = Color.lerp(inactiveColor, activeColor, colorValue)!; + } + // Blend the thumb color against a `surfaceColor` background in case the // thumbColor is not opaque. This way we do not see through the thumb to the // track underneath. @@ -1289,7 +1333,7 @@ class _SwitchPainter extends ToggleablePainter { _paintThumbWith( thumbPaintOffset, canvas, - currentValue, + colorValue, thumbColor, thumbImage, thumbErrorListener, @@ -1381,7 +1425,7 @@ class _SwitchPainter extends ToggleablePainter { thumbPainter.paint( canvas, - thumbPaintOffset + Offset(0, inset), + thumbPaintOffset, configuration.copyWith(size: thumbSize), ); diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index 533b482f90..98b6340ae0 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -2438,6 +2438,89 @@ void main() { ); }); + testWidgets('Switch thumb shows correct pressed color - M3', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(useMaterial3: true); + final ColorScheme colors = themeData.colorScheme; + Widget buildApp({bool enabled = true, bool value = 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, + ); + }), + ), + ), + ); + } + + await tester.pumpWidget(buildApp()); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect(Material.of(tester.element(find.byType(Switch))), + paints..rrect( + color: colors.primary, // track color + style: PaintingStyle.fill, + )..rrect( + color: Colors.transparent, // track outline color + style: PaintingStyle.stroke, + )..rrect(color: colors.primaryContainer, rrect: RRect.fromLTRBR(26.0, 10.0, 54.0, 38.0, const Radius.circular(14.0))), + ); + + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildApp(value: false)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect(Material.of(tester.element(find.byType(Switch))), + paints..rrect( + color: colors.surfaceVariant, // track color + style: PaintingStyle.fill + )..rrect( + color: colors.outline, // track outline color + style: PaintingStyle.stroke, + )..rrect(color: colors.onSurfaceVariant), + ); + + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildApp(enabled: false)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect(Material.of(tester.element(find.byType(Switch))), + paints..rrect( + color: colors.onSurface.withOpacity(0.12), // track color + style: PaintingStyle.fill, + )..rrect( + color: Colors.transparent, // track outline color + style: PaintingStyle.stroke, + )..rrect(color: colors.surface.withOpacity(1.0)), + ); + + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildApp(enabled: false, value: false)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect(Material.of(tester.element(find.byType(Switch))), + paints..rrect( + color: colors.surfaceVariant.withOpacity(0.12), // track color + style: PaintingStyle.fill, + )..rrect( + color: colors.onSurface.withOpacity(0.12), // track outline color + style: PaintingStyle.stroke, + )..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), + ); + }, variant: TargetPlatformVariant.mobile()); + 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;