Added Switch Animation for Material 3 (#113090)

This commit is contained in:
Qun Cheng 2022-10-12 11:02:08 -04:00 committed by GitHub
parent a5ee67ef86
commit 91d88336dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 466 additions and 183 deletions

View File

@ -202,6 +202,18 @@ class _SwitchConfigM3 with _SwitchConfig {
@override
double get trackWidth => ${tokens['md.comp.switch.track.width']};
// The thumb size at the middle of the track. Hand coded default based on the animation specs.
@override
Size get transitionalThumbSize => const Size(34, 22);
// Hand coded default based on the animation specs.
@override
int get toggleDuration => 300;
// Hand coded default based on the animation specs.
@override
double? get thumbOffset => null;
}
''';

View File

@ -666,13 +666,8 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
bool _isPressed = false;
void _handleDragStart(DragStartDetails details) {
if (isInteractive) {
setState(() {
_isPressed = true;
});
reactionController.forward();
}
}
@ -707,9 +702,6 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
} else {
animateToValue();
}
setState(() {
_isPressed = false;
});
reactionController.reverse();
}
@ -734,6 +726,13 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
final _SwitchConfig switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
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.
final Set<MaterialState> activeStates = states..add(MaterialState.selected);
@ -829,7 +828,6 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..isPressed = _isPressed || downPosition != null
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activeThumbImage = widget.activeThumbImage
@ -847,6 +845,7 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..inactiveThumbRadius = effectiveInactiveThumbRadius
..activeThumbRadius = effectiveActiveThumbRadius
..pressedThumbRadius = switchConfig.pressedThumbRadius
..thumbOffset = switchConfig.thumbOffset
..trackHeight = switchConfig.trackHeight
..trackWidth = switchConfig.trackWidth
..activeIconColor = effectiveActiveIconColor
@ -854,7 +853,9 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
..activeIcon = effectiveActiveIcon
..inactiveIcon = effectiveInactiveIcon
..iconTheme = IconTheme.of(context)
..thumbShadow = switchConfig.thumbShadow,
..thumbShadow = switchConfig.thumbShadow
..transitionalThumbSize = switchConfig.transitionalThumbSize
..positionController = positionController,
),
),
);
@ -862,6 +863,17 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
}
class _SwitchPainter extends ToggleablePainter {
AnimationController get positionController => _positionController!;
AnimationController? _positionController;
set positionController(AnimationController? value) {
assert(value != null);
if (value == _positionController) {
return;
}
_positionController = value;
notifyListeners();
}
Icon? get activeIcon => _activeIcon;
Icon? _activeIcon;
set activeIcon(Icon? value) {
@ -914,16 +926,6 @@ class _SwitchPainter extends ToggleablePainter {
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) {
@ -957,6 +959,27 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners();
}
double? get thumbOffset => _thumbOffset;
double? _thumbOffset;
set thumbOffset(double? value) {
if (value == _thumbOffset) {
return;
}
_thumbOffset = value;
notifyListeners();
}
Size get transitionalThumbSize => _transitionalThumbSize!;
Size? _transitionalThumbSize;
set transitionalThumbSize(Size value) {
assert(value != null);
if (value == _transitionalThumbSize) {
return;
}
_transitionalThumbSize = value;
notifyListeners();
}
double get trackHeight => _trackHeight!;
double? _trackHeight;
set trackHeight(double value) {
@ -1119,12 +1142,12 @@ class _SwitchPainter extends ToggleablePainter {
ImageErrorListener? _cachedThumbErrorListener;
BoxPainter? _cachedThumbPainter;
BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) {
return BoxDecoration(
ShapeDecoration _createDefaultThumbDecoration(Color color, ImageProvider? image, ImageErrorListener? errorListener) {
return ShapeDecoration(
color: color,
image: image == null ? null : DecorationImage(image: image, onError: errorListener),
shape: BoxShape.circle,
boxShadow: thumbShadow,
shape: const StadiumBorder(),
shadows: thumbShadow,
);
}
@ -1140,6 +1163,10 @@ class _SwitchPainter extends ToggleablePainter {
}
}
bool _stopPressAnimation = false;
double? _pressedInactiveThumbRadius;
double? _pressedActiveThumbRadius;
@override
void paint(Canvas canvas, Size size) {
final double currentValue = position.value;
@ -1153,10 +1180,88 @@ class _SwitchPainter extends ToggleablePainter {
visualPosition = currentValue;
break;
}
if (reaction.status == AnimationStatus.reverse && _stopPressAnimation == false) {
_stopPressAnimation = true;
} else {
_stopPressAnimation = false;
}
// 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) {
// This happens when the thumb is dragged instead of being tapped.
_pressedInactiveThumbRadius = lerpDouble(inactiveThumbRadius, pressedThumbRadius, reaction.value);
_pressedActiveThumbRadius = lerpDouble(activeThumbRadius, pressedThumbRadius, reaction.value);
}
if (currentValue == 0) {
_pressedInactiveThumbRadius = lerpDouble(inactiveThumbRadius, pressedThumbRadius, reaction.value);
_pressedActiveThumbRadius = activeThumbRadius;
}
if (currentValue == 1) {
_pressedActiveThumbRadius = lerpDouble(activeThumbRadius, pressedThumbRadius, reaction.value);
_pressedInactiveThumbRadius = inactiveThumbRadius;
}
}
final Size inactiveThumbSize = Size.fromRadius(_pressedInactiveThumbRadius ?? inactiveThumbRadius);
final Size activeThumbSize = Size.fromRadius(_pressedActiveThumbRadius ?? activeThumbRadius);
Animation<Size> thumbSizeAnimation(bool isForward) {
List<TweenSequenceItem<Size>> thumbSizeSequence;
if (isForward) {
thumbSizeSequence = <TweenSequenceItem<Size>>[
TweenSequenceItem<Size>(
tween: Tween<Size>(begin: inactiveThumbSize, end: transitionalThumbSize)
.chain(CurveTween(curve: const Cubic(0.31, 0.00, 0.56, 1.00))),
weight: 11,
),
TweenSequenceItem<Size>(
tween: Tween<Size>(begin: transitionalThumbSize, end: activeThumbSize)
.chain(CurveTween(curve: const Cubic(0.20, 0.00, 0.00, 1.00))),
weight: 72,
),
TweenSequenceItem<Size>(
tween: ConstantTween<Size>(activeThumbSize),
weight: 17,
)
];
} else {
thumbSizeSequence = <TweenSequenceItem<Size>>[
TweenSequenceItem<Size>(
tween: ConstantTween<Size>(inactiveThumbSize),
weight: 17,
),
TweenSequenceItem<Size>(
tween: Tween<Size>(begin: inactiveThumbSize, end: transitionalThumbSize)
.chain(CurveTween(curve: const Cubic(0.20, 0.00, 0.00, 1.00).flipped)),
weight: 72,
),
TweenSequenceItem<Size>(
tween: Tween<Size>(begin: transitionalThumbSize, end: activeThumbSize)
.chain(CurveTween(curve: const Cubic(0.31, 0.00, 0.56, 1.00).flipped)),
weight: 11,
),
];
}
return TweenSequence<Size>(thumbSizeSequence).animate(positionController);
}
Size thumbSize;
if (reaction.status == AnimationStatus.completed) {
thumbSize = Size.fromRadius(pressedThumbRadius);
} else {
if (position.status == AnimationStatus.dismissed || position.status == AnimationStatus.forward) {
thumbSize = thumbSizeAnimation(true).value;
} else {
thumbSize = thumbSizeAnimation(false).value;
}
}
// The thumb contracts slightly during the animation in Material 2.
final double inset = thumbOffset == null ? 0 : 1.0 - (currentValue - thumbOffset!).abs() * 2.0;
thumbSize = Size(thumbSize.width - inset, thumbSize.height - inset);
final double thumbRadius = isPressed
? pressedThumbRadius
: lerpDouble(inactiveThumbRadius, activeThumbRadius, currentValue)!;
final Color trackColor = Color.lerp(inactiveTrackColor, activeTrackColor, currentValue)!;
final Color? trackOutlineColor = inactiveTrackOutlineColor == null ? null
: Color.lerp(inactiveTrackOutlineColor, Colors.transparent, currentValue);
@ -1176,8 +1281,8 @@ class _SwitchPainter extends ToggleablePainter {
..color = trackColor;
final Offset trackPaintOffset = _computeTrackPaintOffset(size, trackWidth, trackHeight);
final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, visualPosition);
final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + thumbRadius, size.height / 2);
final Offset thumbPaintOffset = _computeThumbPaintOffset(trackPaintOffset, thumbSize, visualPosition);
final Offset radialReactionOrigin = Offset(thumbPaintOffset.dx + thumbSize.height / 2, size.height / 2);
_paintTrackWith(canvas, paint, trackPaintOffset, trackOutlineColor);
paintRadialReaction(canvas: canvas, origin: radialReactionOrigin);
@ -1188,8 +1293,9 @@ class _SwitchPainter extends ToggleablePainter {
thumbColor,
thumbImage,
thumbErrorListener,
thumbRadius,
thumbIcon,
thumbSize,
inset,
);
}
@ -1203,14 +1309,14 @@ class _SwitchPainter extends ToggleablePainter {
/// Computes canvas offset for thumb's upper left corner as if it were a
/// square
Offset _computeThumbPaintOffset(Offset trackPaintOffset, double visualPosition) {
Offset _computeThumbPaintOffset(Offset trackPaintOffset, Size thumbSize, double visualPosition) {
// How much thumb radius extends beyond the track
final double trackRadius = trackHeight / 2;
final double thumbRadius = isPressed ? pressedThumbRadius : lerpDouble(inactiveThumbRadius, activeThumbRadius, position.value)!;
final double additionalThumbRadius = thumbRadius - trackRadius;
final double additionalThumbRadius = thumbSize.height / 2 - trackRadius;
final double additionalRectWidth = (thumbSize.width - thumbSize.height) / 2;
final double horizontalProgress = visualPosition * trackInnerLength;
final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius + horizontalProgress;
final double thumbHorizontalOffset = trackPaintOffset.dx - additionalThumbRadius - additionalRectWidth + horizontalProgress;
final double thumbVerticalOffset = trackPaintOffset.dy - additionalThumbRadius;
return Offset(thumbHorizontalOffset, thumbVerticalOffset);
@ -1258,8 +1364,9 @@ class _SwitchPainter extends ToggleablePainter {
Color thumbColor,
ImageProvider? thumbImage,
ImageErrorListener? thumbErrorListener,
double thumbRadius,
Icon? thumbIcon,
Size thumbSize,
double inset,
) {
try {
_isPainting = true;
@ -1272,14 +1379,10 @@ class _SwitchPainter extends ToggleablePainter {
}
final BoxPainter thumbPainter = _cachedThumbPainter!;
// The thumb contracts slightly during the animation
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
final double radius = thumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPaintOffset + Offset(0, inset),
configuration.copyWith(size: Size.fromRadius(radius)),
configuration.copyWith(size: thumbSize),
);
if (thumbIcon != null && thumbIcon.icon != null) {
@ -1314,8 +1417,9 @@ class _SwitchPainter extends ToggleablePainter {
text: textSpan,
);
textPainter.layout();
final double additionalIconRadius = thumbRadius - iconSize / 2;
final Offset offset = thumbPaintOffset + Offset(additionalIconRadius, additionalIconRadius);
final double additionalHorizontalOffset = (thumbSize.width - iconSize) / 2;
final double additionalVerticalOffset = (thumbSize.height - iconSize) / 2;
final Offset offset = thumbPaintOffset + Offset(additionalHorizontalOffset, additionalVerticalOffset);
textPainter.paint(canvas, offset);
}
@ -1348,6 +1452,9 @@ mixin _SwitchConfig {
List<BoxShadow>? get thumbShadow;
MaterialStateProperty<Color?>? get trackOutlineColor;
MaterialStateProperty<Color> get iconColor;
double? get thumbOffset;
Size get transitionalThumbSize;
int get toggleDuration;
}
// Hand coded defaults based on Material Design 2.
@ -1389,6 +1496,15 @@ class _SwitchConfigM2 with _SwitchConfig {
@override
double get trackWidth => 33.0;
@override
double get thumbOffset => 0.5;
@override
Size get transitionalThumbSize => const Size(20, 20);
@override
int get toggleDuration => 200;
}
class _SwitchDefaultsM2 extends SwitchThemeData {
@ -1658,6 +1774,18 @@ class _SwitchConfigM3 with _SwitchConfig {
@override
double get trackWidth => 52.0;
// The thumb size at the middle of the track. Hand coded default based on the animation specs.
@override
Size get transitionalThumbSize => const Size(34, 22);
// Hand coded default based on the animation specs.
@override
int get toggleDuration => 300;
// Hand coded default based on the animation specs.
@override
double? get thumbOffset => null;
}
// END GENERATED TOKEN PROPERTIES - Switch

View File

@ -148,10 +148,10 @@ void main() {
find.byType(Switch),
paints
..rrect(color: Colors.blue[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.yellow[500]),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.yellow[500]),
);
await tester.tap(find.byType(Switch));
@ -161,10 +161,10 @@ void main() {
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(color: Colors.green[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.red[500]),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.red[500]),
);
});

View File

@ -378,10 +378,10 @@ void main() {
color: const Color(0x52000000), // Black with 32% opacity
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.grey.shade50),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.grey.shade50),
reason: 'Inactive enabled switch should match these colors',
);
await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
@ -394,10 +394,10 @@ void main() {
color: const Color(0x802196f3),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff2196f3)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xff2196f3)),
reason: 'Active enabled switch should match these colors',
);
});
@ -428,10 +428,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.grey.shade400),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.grey.shade400),
reason: 'Inactive disabled switch should match these colors',
);
@ -460,10 +460,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.grey.shade400),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.grey.shade400),
reason: 'Active disabled switch should match these colors',
);
});
@ -504,10 +504,10 @@ void main() {
color: Colors.blue[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.yellow[500]),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.yellow[500]),
);
await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
await tester.pump();
@ -519,10 +519,10 @@ void main() {
color: Colors.green[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: Colors.red[500]),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: Colors.red[500]),
);
});
@ -838,10 +838,10 @@ void main() {
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff2196f3)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xff2196f3)),
);
// Check the false value.
@ -857,10 +857,10 @@ void main() {
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xfffafafa)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xfffafafa)),
);
// Check what happens when disabled.
@ -875,10 +875,10 @@ void main() {
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xffbdbdbd)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xffbdbdbd)),
);
});
@ -942,10 +942,10 @@ void main() {
color: const Color(0x802196f3),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff2196f3)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xff2196f3)),
);
// Start hovering
@ -963,10 +963,10 @@ void main() {
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: Colors.orange[500])
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xff2196f3)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xff2196f3)),
);
// Check what happens when disabled.
@ -979,10 +979,10 @@ void main() {
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0xffbdbdbd)),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: const Color(0xffbdbdbd)),
);
});
@ -1228,10 +1228,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveDisabledThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
@ -1245,10 +1245,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: activeDisabledThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: activeDisabledThumbColor),
reason: 'Active disabled switch should match these colors',
);
@ -1262,10 +1262,10 @@ void main() {
color: const Color(0x52000000), // Black with 32% opacity,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveEnabledThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: inactiveEnabledThumbColor),
reason: 'Inactive enabled switch should match these colors',
);
@ -1279,10 +1279,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: inactiveDisabledThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should match these colors',
);
});
@ -1338,10 +1338,10 @@ void main() {
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: focusedThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: focusedThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
@ -1359,10 +1359,10 @@ void main() {
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x1f000000))
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: hoveredThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: hoveredThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
});
@ -1577,10 +1577,10 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..circle(color: const Color(0x33000000))
..circle(color: const Color(0x24000000))
..circle(color: const Color(0x1f000000))
..circle(color: expectedThumbColor),
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
..rrect(color: expectedThumbColor),
reason: 'Active disabled thumb color should be blended on top of surface color',
);
});
@ -1934,6 +1934,140 @@ void main() {
});
group('Switch M3 tests', () {
testWidgets('M3 Switch has a 300-millisecond animation in total', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
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(
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
expect(value, isFalse);
final Rect switchRect = tester.getRect(find.byType(Switch));
final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
await tester.pump();
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 200)); // M2 animation duration
expect(tester.hasRunningAnimations, true);
await tester.pump(const Duration(milliseconds: 101));
expect(tester.hasRunningAnimations, false);
});
testWidgets('M3 Switch has a stadium shape in the middle of the track', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true, colorSchemeSeed: Colors.deepPurple);
bool value = false;
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
expect(value, isFalse);
final Rect switchRect = tester.getRect(find.byType(Switch));
final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
await tester.pump();
await gesture.up();
await tester.pump();
// After 33 milliseconds, the switch thumb moves to the middle
// and has a stadium shape with a size of (34x22).
await tester.pump(const Duration(milliseconds: 33));
expect(tester.hasRunningAnimations, true);
await expectLater(
find.byType(Switch),
matchesGoldenFile('switch_test.m3.transition.png'),
);
});
testWidgets('M3 Switch thumb bounces in the end of the animation', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
bool value = false;
await tester.pumpWidget(
MaterialApp(
theme: theme,
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Switch(
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
expect(value, isFalse);
final Rect switchRect = tester.getRect(find.byType(Switch));
final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
await tester.pump();
await gesture.up();
await tester.pump();
// The value on y axis is greater than 1 when t > 0.375
// 300 * 0.375 = 112.5
await tester.pump(const Duration(milliseconds: 113));
final ToggleableStateMixin state = tester.state<ToggleableStateMixin>(
find.descendant(
of: find.byType(Switch),
matching: find.byWidgetPredicate(
(Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch',
),
),
);
expect(tester.hasRunningAnimations, true);
expect(state.position.value, greaterThan(1));
});
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;
@ -1978,7 +2112,7 @@ void main() {
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
..rrect(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));
@ -1993,7 +2127,8 @@ void main() {
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
..rrect()
..rrect(color: colors.onPrimary), // thumb color
reason: 'Active enabled switch should match these colors',
);
});
@ -2035,7 +2170,7 @@ void main() {
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
..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), // thumb color
reason: 'Inactive disabled switch should match these colors',
);
});
@ -2073,7 +2208,8 @@ void main() {
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
..rrect()
..rrect(color: colors.surface), // thumb color
reason: 'Active disabled switch should match these colors',
);
});
@ -2125,7 +2261,7 @@ void main() {
style: PaintingStyle.stroke,
color: colors.outline,
)
..circle(color: Colors.yellow[500]), // thumb color
..rrect(color: Colors.yellow[500]), // thumb color
);
await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
await tester.pump();
@ -2138,7 +2274,8 @@ void main() {
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
..rrect()
..rrect(color: Colors.red[500]), // thumb color
);
});
@ -2225,7 +2362,7 @@ void main() {
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)),
..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)),
);
});
@ -2265,7 +2402,8 @@ void main() {
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: colors.onPrimary),
..rrect()
..rrect(color: colors.onPrimary),
);
// Start hovering
@ -2295,7 +2433,8 @@ void main() {
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)),
..rrect()
..rrect(color: colors.surface.withOpacity(1.0)),
);
});
@ -2359,7 +2498,7 @@ void main() {
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),
..rrect(color: inactiveDisabledThumbColor),
reason: 'Inactive disabled switch should default track and custom thumb color',
);
@ -2374,7 +2513,8 @@ void main() {
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),
..rrect()
..rrect(color: activeDisabledThumbColor),
reason: 'Active disabled switch should match these colors',
);
@ -2389,7 +2529,8 @@ void main() {
color: colors.surfaceVariant,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: inactiveEnabledThumbColor),
..rrect()
..rrect(color: inactiveEnabledThumbColor),
reason: 'Inactive enabled switch should match these colors',
);
@ -2404,7 +2545,8 @@ void main() {
color: colors.primary,
rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
)
..circle(color: activeEnabledThumbColor),
..rrect()
..rrect(color: activeEnabledThumbColor),
reason: 'Active enabled switch should match these colors',
);
});
@ -2465,7 +2607,7 @@ void main() {
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),
..rrect(color: focusedThumbColor),
reason: 'active enabled switch should default track and custom thumb color',
);
@ -2484,7 +2626,7 @@ void main() {
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),
..rrect(color: hoveredThumbColor),
reason: 'active enabled switch should default track and custom thumb color',
);
});
@ -2708,7 +2850,8 @@ void main() {
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),
..rrect()
..rrect(color: expectedThumbColor),
reason: 'Active disabled thumb color should be blended on top of surface color',
);
});
@ -2755,7 +2898,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..circle()
..rrect()..rrect()
..paragraph(offset: const Offset(32.0, 16.0)),
);
@ -2766,7 +2909,7 @@ void main() {
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()
..circle()
..rrect()
..paragraph(offset: const Offset(12.0, 16.0)),
);
@ -2776,7 +2919,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()..circle()
..rrect()..rrect()..rrect()
);
// inactive icon doesn't show when switch is on.
@ -2785,7 +2928,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..circle()..restore(),
..rrect()..rrect()..restore(),
);
// without icon
@ -2793,7 +2936,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect()..rrect()..circle()..restore(),
..rrect()..rrect()..rrect()..restore(),
);
});
});

View File

@ -147,15 +147,15 @@ void main() {
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor)
..rrect(color: defaultThumbColor)
..paragraph()
)
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor)
..rrect()
..rrect()
..rrect()
..rrect(color: defaultThumbColor)
)
);
// Size from MaterialTapTargetSize.shrinkWrap.
@ -168,14 +168,14 @@ void main() {
_getSwitchMaterial(tester),
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor)..paragraph())
..rrect(color: selectedTrackColor)..rrect()
..rrect(color: selectedThumbColor)..paragraph())
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: selectedThumbColor))
);
// Switch with hover.
@ -295,13 +295,13 @@ void main() {
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor)..paragraph(offset: const Offset(12, 16)))
..rrect(color: defaultThumbColor)..paragraph(offset: const Offset(12, 16)))
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: defaultThumbColor))
);
// Size from MaterialTapTargetSize.shrinkWrap.
expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0));
@ -314,13 +314,13 @@ void main() {
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
..rrect(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: selectedThumbColor))
);
// Switch with hover.
@ -393,13 +393,13 @@ void main() {
? (paints
..rrect(color: defaultTrackColor)
..rrect(color: themeData.colorScheme.outline)
..circle(color: defaultThumbColor))
..rrect(color: defaultThumbColor))
: (paints
..rrect(color: defaultTrackColor)
..circle()
..circle()
..circle()
..circle(color: defaultThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: defaultThumbColor))
);
// Selected switch.
@ -410,13 +410,13 @@ void main() {
material3
? (paints
..rrect(color: selectedTrackColor)
..circle(color: selectedThumbColor))
..rrect(color: selectedThumbColor))
: (paints
..rrect(color: selectedTrackColor)
..circle()
..circle()
..circle()
..circle(color: selectedThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: selectedThumbColor))
);
});
@ -532,13 +532,13 @@ void main() {
material3
? (paints
..rrect(color: localThemeTrackColor)
..circle(color: localThemeThumbColor))
..rrect(color: localThemeThumbColor))
: (paints
..rrect(color: localThemeTrackColor)
..circle()
..circle()
..circle()
..circle(color: localThemeThumbColor))
..rrect()
..rrect()
..rrect()
..rrect(color: localThemeThumbColor))
);
});
}