Adaptive Switch (#130425)

Currently, `Switch.factory` delegates to `CupertinoSwitch` when platform
is iOS or macOS. This PR is to:
* have the factory configure the Material `Switch` for the expected look
and feel.
* introduce `Adaptation` class to customize themes for the adaptive
components.
This commit is contained in:
Qun Cheng 2023-11-07 10:26:23 -08:00 committed by GitHub
parent a76720e9f6
commit ed70f4e248
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1470 additions and 273 deletions

View file

@ -126,6 +126,12 @@ class _${blockName}DefaultsM3 extends SwitchThemeData {
});
}
@override
MaterialStateProperty<MouseCursor> get mouseCursor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states)
=> MaterialStateMouseCursor.clickable.resolve(states));
}
@override
MaterialStatePropertyAll<double> get trackOutlineWidth => const MaterialStatePropertyAll<double>(${getToken('md.comp.switch.track.outline.width')});

View file

@ -0,0 +1,134 @@
// 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 'package:flutter/material.dart';
/// Flutter code sample for [Switch.adaptive].
void main() => runApp(const SwitchApp());
class SwitchApp extends StatefulWidget {
const SwitchApp({super.key});
@override
State<SwitchApp> createState() => _SwitchAppState();
}
class _SwitchAppState extends State<SwitchApp> {
bool isMaterial = true;
bool isCustomized = false;
@override
Widget build(BuildContext context) {
final ThemeData theme = ThemeData(
platform: isMaterial ? TargetPlatform.android : TargetPlatform.iOS,
adaptations: <Adaptation<Object>>[
if (isCustomized) const _SwitchThemeAdaptation()
]
);
final ButtonStyle style = OutlinedButton.styleFrom(
fixedSize: const Size(220, 40),
);
return MaterialApp(
theme: theme,
home: Scaffold(
appBar: AppBar(title: const Text('Adaptive Switches')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
style: style,
onPressed: () {
setState(() {
isMaterial = !isMaterial;
});
},
child: isMaterial ? const Text('Show cupertino style') : const Text('Show material style'),
),
OutlinedButton(
style: style,
onPressed: () {
setState(() {
isCustomized = !isCustomized;
});
},
child: isCustomized ? const Text('Remove customization') : const Text('Add customization'),
),
const SizedBox(height: 20),
const SwitchWithLabel(label: 'enabled', enabled: true),
const SwitchWithLabel(label: 'disabled', enabled: false),
],
),
),
);
}
}
class SwitchWithLabel extends StatefulWidget {
const SwitchWithLabel({
super.key,
required this.enabled,
required this.label,
});
final bool enabled;
final String label;
@override
State<SwitchWithLabel> createState() => _SwitchWithLabelState();
}
class _SwitchWithLabelState extends State<SwitchWithLabel> {
bool active = true;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 150,
padding: const EdgeInsets.only(right: 20),
child: Text(widget.label)
),
Switch.adaptive(
value: active,
onChanged: !widget.enabled ? null : (bool value) {
setState(() {
active = value;
});
},
),
],
);
}
}
class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
const _SwitchThemeAdaptation();
@override
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return defaultValue;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return Colors.yellow;
}
return null; // Use the default.
}),
trackColor: const MaterialStatePropertyAll<Color>(Colors.brown),
);
}
}
}

View file

@ -0,0 +1,63 @@
// 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 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/switch/switch.4.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Show adaptive switch theme', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SwitchApp(),
);
// Default is material style switches
expect(find.text('Show cupertino style'), findsOneWidget);
expect(find.text('Show material style'), findsNothing);
Finder adaptiveSwitch = find.byType(Switch).first;
expect(
adaptiveSwitch,
paints
..rrect(color: const Color(0xff6750a4)) // M3 primary color.
..rrect()
..rrect(color: Colors.white), // Thumb color
);
await tester.tap(find.widgetWithText(OutlinedButton, 'Add customization'));
await tester.pumpAndSettle();
// Theme adaptation does not affect material-style switch.
adaptiveSwitch = find.byType(Switch).first;
expect(
adaptiveSwitch,
paints
..rrect(color: const Color(0xff6750a4)) // M3 primary color.
..rrect()
..rrect(color: Colors.white), // Thumb color
);
await tester.tap(find.widgetWithText(OutlinedButton, 'Show cupertino style'));
await tester.pumpAndSettle();
expect(
adaptiveSwitch,
paints
..rrect(color: const Color(0xff795548)) // Customized track color only for cupertino.
..rrect()..rrect()..rrect()..rrect()
..rrect(color: const Color(0xffffeb3b)), // Customized thumb color only for cupertino.
);
await tester.tap(find.widgetWithText(OutlinedButton, 'Remove customization'));
await tester.pumpAndSettle();
expect(
adaptiveSwitch,
paints
..rrect(color: const Color(0xff34c759)) // Cupertino system green.
..rrect()..rrect()..rrect()..rrect()
..rrect(color: Colors.white), // Thumb color
);
});
}

View file

@ -134,15 +134,21 @@ class Switch extends StatelessWidget {
/// or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// On iOS and macOS, this constructor creates a [CupertinoSwitch], which has
/// matching functionality and presentation as Material switches, and are the
/// graphics expected on iOS. On other platforms, this creates a Material
/// design [Switch].
/// Creates a switch that looks and feels native when the [ThemeData.platform]
/// is iOS or macOS, otherwise a Material Design switch is created.
///
/// If a [CupertinoSwitch] is created, the following parameters are ignored:
/// [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor], [trackOutlineWidth]
/// [activeThumbImage], [onActiveThumbImageError], [inactiveThumbImage],
/// [onInactiveThumbImageError], [materialTapTargetSize].
/// To provide a custom switch theme that's only used by this factory
/// constructor, add a custom `Adaptation<SwitchThemeData>` class to
/// [ThemeData.adaptations]. This can be useful in situations where you don't
/// want the overall [ThemeData.switchTheme] to apply when this adaptive
/// constructor is used.
///
/// {@tool dartpad}
/// This sample shows how to create and use subclasses of [Adaptation] that
/// define adaptive [SwitchThemeData]s.
///
/// ** See code in examples/api/lib/material/switch/switch.4.dart **
/// {@end-tool}
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const Switch.adaptive({
@ -220,8 +226,6 @@ class Switch extends StatelessWidget {
///
/// Defaults to [ColorScheme.secondary] with the opacity set at 50%.
///
/// Ignored if this switch is created with [Switch.adaptive].
///
/// If [trackColor] returns a non-null color in the [MaterialState.selected]
/// state, it will be used instead of this color.
final Color? activeTrackColor;
@ -232,8 +236,6 @@ class Switch extends StatelessWidget {
///
/// Defaults to the colors described in the Material design specification.
///
/// Ignored if this switch is created with [Switch.adaptive].
///
/// If [thumbColor] returns a non-null color in the default state, it will be
/// used instead of this color.
final Color? inactiveThumbColor;
@ -244,8 +246,6 @@ class Switch extends StatelessWidget {
///
/// Defaults to the colors described in the Material design specification.
///
/// Ignored if this switch is created with [Switch.adaptive].
///
/// If [trackColor] returns a non-null color in the default state, it will be
/// used instead of this color.
final Color? inactiveTrackColor;
@ -253,8 +253,6 @@ class Switch extends StatelessWidget {
/// {@template flutter.material.switch.activeThumbImage}
/// An image to use on the thumb of this switch when the switch is on.
/// {@endtemplate}
///
/// Ignored if this switch is created with [Switch.adaptive].
final ImageProvider? activeThumbImage;
/// {@template flutter.material.switch.onActiveThumbImageError}
@ -266,8 +264,6 @@ class Switch extends StatelessWidget {
/// {@template flutter.material.switch.inactiveThumbImage}
/// An image to use on the thumb of this switch when the switch is off.
/// {@endtemplate}
///
/// Ignored if this switch is created with [Switch.adaptive].
final ImageProvider? inactiveThumbImage;
/// {@template flutter.material.switch.onInactiveThumbImageError}
@ -559,7 +555,12 @@ class Switch extends StatelessWidget {
Size _getSwitchSize(BuildContext context) {
final ThemeData theme = Theme.of(context);
final SwitchThemeData switchTheme = SwitchTheme.of(context);
SwitchThemeData switchTheme = SwitchTheme.of(context);
if (_switchType == _SwitchType.adaptive) {
final Adaptation<SwitchThemeData> switchAdaptation = theme.getAdaptation<SwitchThemeData>()
?? const _SwitchThemeAdaptation();
switchTheme = switchAdaptation.adapt(theme, switchTheme);
}
final _SwitchConfig switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
@ -573,35 +574,32 @@ class Switch extends StatelessWidget {
}
}
Widget _buildCupertinoSwitch(BuildContext context) {
final Size size = _getSwitchSize(context);
return Container(
width: size.width, // Same size as the Material switch.
height: size.height,
alignment: Alignment.center,
child: CupertinoSwitch(
dragStartBehavior: dragStartBehavior,
value: value,
onChanged: onChanged,
activeColor: activeColor,
trackColor: inactiveTrackColor,
thumbColor: thumbColor?.resolve(<MaterialState>{}),
applyTheme: applyCupertinoTheme,
focusColor: focusColor,
focusNode: focusNode,
onFocusChange: onFocusChange,
autofocus: autofocus,
),
);
}
@override
Widget build(BuildContext context) {
Color? effectiveActiveThumbColor;
Color? effectiveActiveTrackColor;
Widget _buildMaterialSwitch(BuildContext context) {
switch (_switchType) {
case _SwitchType.material:
effectiveActiveThumbColor = activeColor;
case _SwitchType.adaptive:
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
effectiveActiveThumbColor = activeColor;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
effectiveActiveTrackColor = activeColor;
}
}
return _MaterialSwitch(
value: value,
onChanged: onChanged,
size: _getSwitchSize(context),
activeColor: activeColor,
activeTrackColor: activeTrackColor,
activeColor: effectiveActiveThumbColor,
activeTrackColor: activeTrackColor ?? effectiveActiveTrackColor,
inactiveThumbColor: inactiveThumbColor,
inactiveTrackColor: inactiveTrackColor,
activeThumbImage: activeThumbImage,
@ -623,31 +621,11 @@ class Switch extends StatelessWidget {
focusNode: focusNode,
onFocusChange: onFocusChange,
autofocus: autofocus,
applyCupertinoTheme: applyCupertinoTheme,
switchType: _switchType,
);
}
@override
Widget build(BuildContext context) {
switch (_switchType) {
case _SwitchType.material:
return _buildMaterialSwitch(context);
case _SwitchType.adaptive: {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _buildMaterialSwitch(context);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _buildCupertinoSwitch(context);
}
}
}
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
@ -661,6 +639,7 @@ class _MaterialSwitch extends StatefulWidget {
required this.value,
required this.onChanged,
required this.size,
required this.switchType,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
@ -684,8 +663,9 @@ class _MaterialSwitch extends StatefulWidget {
this.focusNode,
this.onFocusChange,
this.autofocus = false,
}) : assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
this.applyCupertinoTheme,
}) : assert(activeThumbImage != null || onActiveThumbImageError == null),
assert(inactiveThumbImage != null || onInactiveThumbImageError == null);
final bool value;
final ValueChanged<bool>? onChanged;
@ -713,6 +693,8 @@ class _MaterialSwitch extends StatefulWidget {
final ValueChanged<bool>? onFocusChange;
final bool autofocus;
final Size size;
final bool? applyCupertinoTheme;
final _SwitchType switchType;
@override
State<StatefulWidget> createState() => _MaterialSwitchState();
@ -728,15 +710,24 @@ 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) {
if (Theme.of(context).useMaterial3) {
position
..curve = Curves.easeOutBack
..reverseCurve = Curves.easeOutBack.flipped;
} else {
position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
switch (widget.switchType) {
case _SwitchType.adaptive:
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
updateCurve();
case TargetPlatform.iOS:
case TargetPlatform.macOS:
position
..curve = Curves.linear
..reverseCurve = Curves.linear;
}
case _SwitchType.material:
updateCurve();
}
}
animateToValue();
}
@ -757,6 +748,18 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
@override
bool? get value => widget.value;
void updateCurve() {
if (Theme.of(context).useMaterial3) {
position
..curve = Curves.easeOutBack
..reverseCurve = Curves.easeOutBack.flipped;
} else {
position
..curve = Curves.easeIn
..reverseCurve = Curves.easeOut;
}
}
MaterialStateProperty<Color?> get _widgetThumbColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
@ -778,7 +781,27 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
});
}
double get _trackInnerLength => widget.size.width - _kSwitchMinSize;
double get _trackInnerLength {
switch (widget.switchType) {
case _SwitchType.adaptive:
switch (Theme.of(context).platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return widget.size.width - _kSwitchMinSize;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
final _SwitchConfig config = _SwitchConfigCupertino(context);
final double trackInnerStart = config.trackHeight / 2.0;
final double trackInnerEnd = config.trackWidth - trackInnerStart;
final double trackInnerLength = trackInnerEnd - trackInnerStart;
return trackInnerLength;
}
case _SwitchType.material:
return widget.size.width - _kSwitchMinSize;
}
}
void _handleDragStart(DragStartDetails details) {
if (isInteractive) {
@ -824,6 +847,8 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
widget.onChanged?.call(value!);
}
bool isCupertino = false;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
@ -834,9 +859,40 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
}
final ThemeData theme = Theme.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);
SwitchThemeData switchTheme = SwitchTheme.of(context);
final Color cupertinoPrimaryColor = theme.cupertinoOverrideTheme?.primaryColor ?? theme.colorScheme.primary;
_SwitchConfig switchConfig;
SwitchThemeData defaults;
bool applyCupertinoTheme = false;
double disabledOpacity = 1;
switch (widget.switchType) {
case _SwitchType.material:
switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
defaults = theme.useMaterial3 ? _SwitchDefaultsM3(context) : _SwitchDefaultsM2(context);
case _SwitchType.adaptive:
final Adaptation<SwitchThemeData> switchAdaptation = theme.getAdaptation<SwitchThemeData>()
?? const _SwitchThemeAdaptation();
switchTheme = switchAdaptation.adapt(theme, switchTheme);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
switchConfig = theme.useMaterial3 ? _SwitchConfigM3(context) : _SwitchConfigM2();
defaults = theme.useMaterial3 ? _SwitchDefaultsM3(context) : _SwitchDefaultsM2(context);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
isCupertino = true;
applyCupertinoTheme = widget.applyCupertinoTheme
?? theme.cupertinoOverrideTheme?.applyThemeToAll
?? false;
disabledOpacity = 0.5;
switchConfig = _SwitchConfigCupertino(context);
defaults = _SwitchDefaultsCupertino(context);
reactionController.duration = const Duration(milliseconds: 200);
}
}
positionController.duration = Duration(milliseconds: switchConfig.toggleDuration);
@ -857,12 +913,12 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
?? defaults.thumbColor!.resolve(inactiveStates)!;
final Color effectiveActiveTrackColor = widget.trackColor?.resolve(activeStates)
?? _widgetTrackColor.resolve(activeStates)
?? switchTheme.trackColor?.resolve(activeStates)
?? (applyCupertinoTheme ? cupertinoPrimaryColor : switchTheme.trackColor?.resolve(activeStates))
?? _widgetThumbColor.resolve(activeStates)?.withAlpha(0x80)
?? defaults.trackColor!.resolve(activeStates)!;
final Color effectiveActiveTrackOutlineColor = widget.trackOutlineColor?.resolve(activeStates)
final Color? effectiveActiveTrackOutlineColor = widget.trackOutlineColor?.resolve(activeStates)
?? switchTheme.trackOutlineColor?.resolve(activeStates)
?? Colors.transparent;
?? defaults.trackOutlineColor!.resolve(activeStates);
final double? effectiveActiveTrackOutlineWidth = widget.trackOutlineWidth?.resolve(activeStates)
?? switchTheme.trackOutlineWidth?.resolve(activeStates)
?? defaults.trackOutlineWidth?.resolve(activeStates);
@ -890,6 +946,12 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? switchTheme.overlayColor?.resolve(focusedStates)
?? (applyCupertinoTheme
? HSLColor
.fromColor(cupertinoPrimaryColor.withOpacity(0.80))
.withLightness(0.69).withSaturation(0.835)
.toColor()
: null)
?? defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
@ -921,7 +983,7 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
final MaterialStateProperty<MouseCursor> effectiveMouseCursor = MaterialStateProperty.resolveWith<MouseCursor>((Set<MaterialState> states) {
return MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
?? switchTheme.mouseCursor?.resolve(states)
?? MaterialStateProperty.resolveAs<MouseCursor>(MaterialStateMouseCursor.clickable, states);
?? defaults.mouseCursor!.resolve(states)!;
});
final double effectiveActiveThumbRadius = effectiveActiveIcon == null ? switchConfig.activeThumbRadius : switchConfig.thumbRadiusWithIcon;
@ -937,58 +999,62 @@ class _MaterialSwitchState extends State<_MaterialSwitch> with TickerProviderSta
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
dragStartBehavior: widget.dragStartBehavior,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
size: widget.size,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = effectiveSplashRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activePressedColor = effectiveActivePressedThumbColor
..inactivePressedColor = effectiveInactivePressedThumbColor
..activeThumbImage = widget.activeThumbImage
..onActiveThumbImageError = widget.onActiveThumbImageError
..inactiveThumbImage = widget.inactiveThumbImage
..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor
..activeTrackOutlineColor = effectiveActiveTrackOutlineColor
..activeTrackOutlineWidth = effectiveActiveTrackOutlineWidth
..inactiveTrackColor = effectiveInactiveTrackColor
..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor
..inactiveTrackOutlineWidth = effectiveInactiveTrackOutlineWidth
..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context)
..surfaceColor = theme.colorScheme.surface
..inactiveThumbRadius = effectiveInactiveThumbRadius
..activeThumbRadius = effectiveActiveThumbRadius
..pressedThumbRadius = switchConfig.pressedThumbRadius
..thumbOffset = switchConfig.thumbOffset
..trackHeight = switchConfig.trackHeight
..trackWidth = switchConfig.trackWidth
..activeIconColor = effectiveActiveIconColor
..inactiveIconColor = effectiveInactiveIconColor
..activeIcon = effectiveActiveIcon
..inactiveIcon = effectiveInactiveIcon
..iconTheme = IconTheme.of(context)
..thumbShadow = switchConfig.thumbShadow
..transitionalThumbSize = switchConfig.transitionalThumbSize
..positionController = positionController,
child: Opacity(
opacity: onChanged == null ? disabledOpacity : 1,
child: buildToggleable(
mouseCursor: effectiveMouseCursor,
focusNode: widget.focusNode,
onFocusChange: widget.onFocusChange,
autofocus: widget.autofocus,
size: widget.size,
painter: _painter
..position = position
..reaction = reaction
..reactionFocusFade = reactionFocusFade
..reactionHoverFade = reactionHoverFade
..inactiveReactionColor = effectiveInactivePressedOverlayColor
..reactionColor = effectiveActivePressedOverlayColor
..hoverColor = effectiveHoverOverlayColor
..focusColor = effectiveFocusOverlayColor
..splashRadius = effectiveSplashRadius
..downPosition = downPosition
..isFocused = states.contains(MaterialState.focused)
..isHovered = states.contains(MaterialState.hovered)
..activeColor = effectiveActiveThumbColor
..inactiveColor = effectiveInactiveThumbColor
..activePressedColor = effectiveActivePressedThumbColor
..inactivePressedColor = effectiveInactivePressedThumbColor
..activeThumbImage = widget.activeThumbImage
..onActiveThumbImageError = widget.onActiveThumbImageError
..inactiveThumbImage = widget.inactiveThumbImage
..onInactiveThumbImageError = widget.onInactiveThumbImageError
..activeTrackColor = effectiveActiveTrackColor
..activeTrackOutlineColor = effectiveActiveTrackOutlineColor
..activeTrackOutlineWidth = effectiveActiveTrackOutlineWidth
..inactiveTrackColor = effectiveInactiveTrackColor
..inactiveTrackOutlineColor = effectiveInactiveTrackOutlineColor
..inactiveTrackOutlineWidth = effectiveInactiveTrackOutlineWidth
..configuration = createLocalImageConfiguration(context)
..isInteractive = isInteractive
..trackInnerLength = _trackInnerLength
..textDirection = Directionality.of(context)
..surfaceColor = theme.colorScheme.surface
..inactiveThumbRadius = effectiveInactiveThumbRadius
..activeThumbRadius = effectiveActiveThumbRadius
..pressedThumbRadius = switchConfig.pressedThumbRadius
..thumbOffset = switchConfig.thumbOffset
..trackHeight = switchConfig.trackHeight
..trackWidth = switchConfig.trackWidth
..activeIconColor = effectiveActiveIconColor
..inactiveIconColor = effectiveInactiveIconColor
..activeIcon = effectiveActiveIcon
..inactiveIcon = effectiveInactiveIcon
..iconTheme = IconTheme.of(context)
..thumbShadow = switchConfig.thumbShadow
..transitionalThumbSize = switchConfig.transitionalThumbSize
..positionController = positionController
..isCupertino = isCupertino,
),
),
),
);
@ -1299,6 +1365,17 @@ class _SwitchPainter extends ToggleablePainter {
notifyListeners();
}
bool get isCupertino => _isCupertino!;
bool? _isCupertino;
set isCupertino(bool? value) {
assert(value != null);
if (value == _isCupertino) {
return;
}
_isCupertino = value;
notifyListeners();
}
List<BoxShadow>? get thumbShadow => _thumbShadow;
List<BoxShadow>? _thumbShadow;
set thumbShadow(List<BoxShadow>? value) {
@ -1320,7 +1397,7 @@ class _SwitchPainter extends ToggleablePainter {
color: color,
image: image == null ? null : DecorationImage(image: image, onError: errorListener),
shape: const StadiumBorder(),
shadows: thumbShadow,
shadows: isCupertino ? null : thumbShadow,
);
}
@ -1339,6 +1416,7 @@ class _SwitchPainter extends ToggleablePainter {
bool _stopPressAnimation = false;
double? _pressedInactiveThumbRadius;
double? _pressedActiveThumbRadius;
late double? _pressedThumbExtension;
@override
void paint(Canvas canvas, Size size) {
@ -1360,6 +1438,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) {
_pressedThumbExtension = isCupertino ? reaction.value * 7 : 0;
if (reaction.isCompleted) {
// This happens when the thumb is dragged instead of being tapped.
_pressedInactiveThumbRadius = lerpDouble(inactiveThumbRadius, pressedThumbRadius, reaction.value);
@ -1374,9 +1453,8 @@ class _SwitchPainter extends ToggleablePainter {
_pressedInactiveThumbRadius = inactiveThumbRadius;
}
}
final Size inactiveThumbSize = Size.fromRadius(_pressedInactiveThumbRadius ?? inactiveThumbRadius);
final Size activeThumbSize = Size.fromRadius(_pressedActiveThumbRadius ?? activeThumbRadius);
final Size inactiveThumbSize = isCupertino ? Size(_pressedInactiveThumbRadius! * 2 + _pressedThumbExtension!, _pressedInactiveThumbRadius! * 2) : Size.fromRadius(_pressedInactiveThumbRadius ?? inactiveThumbRadius);
final Size activeThumbSize = isCupertino ? Size(_pressedActiveThumbRadius! * 2 + _pressedThumbExtension!, _pressedActiveThumbRadius! * 2) : Size.fromRadius(_pressedActiveThumbRadius ?? activeThumbRadius);
Animation<Size> thumbSizeAnimation(bool isForward) {
List<TweenSequenceItem<Size>> thumbSizeSequence;
if (isForward) {
@ -1418,24 +1496,36 @@ class _SwitchPainter extends ToggleablePainter {
return TweenSequence<Size>(thumbSizeSequence).animate(positionController);
}
Size thumbSize;
if (reaction.isCompleted) {
thumbSize = Size.fromRadius(pressedThumbRadius);
} else {
if (position.isDismissed || position.status == AnimationStatus.forward) {
thumbSize = thumbSizeAnimation(true).value;
Size? thumbSize;
if (isCupertino) {
if (reaction.isCompleted) {
thumbSize = Size(_pressedInactiveThumbRadius! * 2 + _pressedThumbExtension!, _pressedInactiveThumbRadius! * 2);
} else {
thumbSize = thumbSizeAnimation(false).value;
if (position.isDismissed || position.status == AnimationStatus.forward) {
thumbSize = Size.lerp(inactiveThumbSize, activeThumbSize, position.value);
} else {
thumbSize = Size.lerp(inactiveThumbSize, activeThumbSize, position.value);
}
}
} else {
if (reaction.isCompleted) {
thumbSize = Size.fromRadius(pressedThumbRadius);
} else {
if (position.isDismissed || 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);
thumbSize = Size(thumbSize!.width - inset, thumbSize.height - inset);
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
final Color? trackOutlineColor = inactiveTrackOutlineColor == null || activeTrackOutlineColor == null ? null
: Color.lerp(inactiveTrackOutlineColor, activeTrackOutlineColor, colorValue);
final double? trackOutlineWidth = lerpDouble(inactiveTrackOutlineWidth, activeTrackOutlineWidth, colorValue);
Color lerpedThumbColor;
@ -1496,12 +1586,10 @@ class _SwitchPainter extends ToggleablePainter {
// How much thumb radius extends beyond the track
final double trackRadius = trackHeight / 2;
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 - additionalRectWidth + horizontalProgress;
final double horizontalProgress = visualPosition * (trackInnerLength - _pressedThumbExtension!);
final double thumbHorizontalOffset = trackPaintOffset.dx + trackRadius + (_pressedThumbExtension! / 2) - thumbSize.width / 2 + horizontalProgress;
final double thumbVerticalOffset = trackPaintOffset.dy - additionalThumbRadius;
return Offset(thumbHorizontalOffset, thumbVerticalOffset);
}
@ -1520,8 +1608,8 @@ class _SwitchPainter extends ToggleablePainter {
canvas.drawRRect(trackRRect, paint);
// paint track outline
if (trackOutlineColor != null) {
// paint track outline
final Rect outlineTrackRect = Rect.fromLTWH(
trackPaintOffset.dx + 1,
trackPaintOffset.dy + 1,
@ -1532,12 +1620,26 @@ class _SwitchPainter extends ToggleablePainter {
outlineTrackRect,
Radius.circular(trackRadius),
);
final Paint outlinePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = trackOutlineWidth ?? 2.0
..color = trackOutlineColor;
canvas.drawRRect(outlineTrackRRect, outlinePaint);
}
if (isCupertino) {
if (isFocused) {
final RRect focusedOutline = trackRRect.inflate(1.75);
final Paint focusedPaint = Paint()
..style = PaintingStyle.stroke
..color = focusColor
..strokeWidth = _kCupertinoFocusTrackOutline;
canvas.drawRRect(focusedOutline, focusedPaint);
}
canvas.clipRRect(trackRRect);
}
}
void _paintThumbWith(
@ -1562,6 +1664,10 @@ class _SwitchPainter extends ToggleablePainter {
}
final BoxPainter thumbPainter = _cachedThumbPainter!;
if (isCupertino) {
_paintCupertinoThumbShadowAndBorder(canvas, thumbPaintOffset, thumbSize);
}
thumbPainter.paint(
canvas,
thumbPaintOffset,
@ -1610,6 +1716,26 @@ class _SwitchPainter extends ToggleablePainter {
}
}
void _paintCupertinoThumbShadowAndBorder(Canvas canvas, Offset thumbPaintOffset, Size thumbSize,) {
final RRect thumbBounds = RRect.fromLTRBR(
thumbPaintOffset.dx,
thumbPaintOffset.dy,
thumbPaintOffset.dx + thumbSize.width,
thumbPaintOffset.dy + thumbSize.height,
Radius.circular(thumbSize.height / 2.0),
);
if (thumbShadow != null) {
for (final BoxShadow shadow in thumbShadow!) {
canvas.drawRRect(thumbBounds.shift(shadow.offset), shadow.toPaint());
}
}
canvas.drawRRect(
thumbBounds.inflate(0.5),
Paint()..color = const Color(0x0A000000),
);
}
@override
void dispose() {
_textPainter.dispose();
@ -1622,6 +1748,24 @@ class _SwitchPainter extends ToggleablePainter {
}
}
class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
const _SwitchThemeAdaptation();
@override
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return defaultValue;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return const SwitchThemeData();
}
}
}
mixin _SwitchConfig {
double get trackHeight;
double get trackWidth;
@ -1639,6 +1783,128 @@ mixin _SwitchConfig {
int get toggleDuration;
}
// Hand coded defaults for iOS/macOS Switch
class _SwitchDefaultsCupertino extends SwitchThemeData {
const _SwitchDefaultsCupertino(this.context);
final BuildContext context;
@override
MaterialStateProperty<MouseCursor?> get mouseCursor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.basic;
}
return kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic;
});
}
@override
MaterialStateProperty<Color> get thumbColor => const MaterialStatePropertyAll<Color>(Colors.white);
@override
MaterialStateProperty<Color> get trackColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return CupertinoDynamicColor.resolve(CupertinoColors.systemGreen, context);
}
return CupertinoDynamicColor.resolve(CupertinoColors.secondarySystemFill, context);
});
}
@override
MaterialStateProperty<Color?> get trackOutlineColor => const MaterialStatePropertyAll<Color>(Colors.transparent);
@override
MaterialStateProperty<Color?> get overlayColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.focused)) {
return HSLColor
.fromColor(CupertinoDynamicColor.resolve(CupertinoColors.systemGreen, context).withOpacity(0.80))
.withLightness(0.69).withSaturation(0.835)
.toColor();
}
return Colors.transparent;
});
}
@override
double get splashRadius => 0.0;
}
const double _kCupertinoFocusTrackOutline = 3.5;
class _SwitchConfigCupertino with _SwitchConfig {
_SwitchConfigCupertino(this.context)
: _colors = Theme.of(context).colorScheme;
BuildContext context;
final ColorScheme _colors;
@override
MaterialStateProperty<Color> get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
return _colors.onPrimaryContainer;
});
}
@override
double get activeThumbRadius => 14.0;
@override
double get inactiveThumbRadius => 14.0;
@override
double get pressedThumbRadius => 14.0;
@override
double get switchHeight => _kSwitchMinSize + 8.0;
@override
double get switchHeightCollapsed => _kSwitchMinSize;
@override
double get switchWidth => 60.0;
@override
double get thumbRadiusWithIcon => 14.0;
@override
List<BoxShadow>? get thumbShadow => const <BoxShadow> [
BoxShadow(
color: Color(0x26000000),
offset: Offset(0, 3),
blurRadius: 8.0,
),
BoxShadow(
color: Color(0x0F000000),
offset: Offset(0, 3),
blurRadius: 1.0,
),
];
@override
double get trackHeight => 31.0;
@override
double get trackWidth => 51.0;
// The thumb size at the middle of the track. Hand coded default based on the animation specs.
@override
Size get transitionalThumbSize => const Size(28.0, 28.0);
// Hand coded default by comparing with [CupertinoSwitch].
@override
int get toggleDuration => 140;
// Hand coded default based on the animation specs.
@override
double? get thumbOffset => null;
}
// Hand coded defaults based on Material Design 2.
class _SwitchConfigM2 with _SwitchConfig {
_SwitchConfigM2();
@ -1727,7 +1993,7 @@ class _SwitchDefaultsM2 extends SwitchThemeData {
}
@override
MaterialStateProperty<Color?>? get trackOutlineColor => null;
MaterialStateProperty<Color?>? get trackOutlineColor => const MaterialStatePropertyAll<Color>(Colors.transparent);
@override
MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize;
@ -1878,6 +2144,12 @@ class _SwitchDefaultsM3 extends SwitchThemeData {
});
}
@override
MaterialStateProperty<MouseCursor> get mouseCursor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states)
=> MaterialStateMouseCursor.clickable.resolve(states));
}
@override
MaterialStatePropertyAll<double> get trackOutlineWidth => const MaterialStatePropertyAll<double>(2.0);

View file

@ -73,6 +73,37 @@ export 'package:flutter/services.dart' show Brightness;
// Examples can assume:
// late BuildContext context;
/// Defines a customized theme for components with an `adaptive` factory constructor.
///
/// Currently, only [Switch.adaptive] supports this class.
class Adaptation<T> {
/// Creates an [Adaptation].
const Adaptation();
/// The adaptation's type.
Type get type => T;
/// Typically, this is overridden to return an instance of a custom component
/// ThemeData class, like [SwitchThemeData], instead of the defaultValue.
///
/// Factory constructors that support adaptations - currently only
/// [Switch.adaptive] - look for a [ThemeData.adaptations] member of the expected
/// type when computing their effective default component theme. If a matching
/// adaptation is not found, the component may choose to use a default adaptation.
/// For example, the [Switch.adaptive] component uses an empty [SwitchThemeData]
/// if a matching adaptation is not found, for the sake of backwards compatibility.
///
/// {@tool dartpad}
/// This sample shows how to create and use subclasses of [Adaptation] that
/// define adaptive [SwitchThemeData]s. The [adapt] method in this example is
/// overridden to only customize cupertino-style switches, but it can also be
/// used to customize any other platforms.
///
/// ** See code in examples/api/lib/material/switch/switch.4.dart **
/// {@end-tool}
T adapt(ThemeData theme, T defaultValue) => defaultValue;
}
/// An interface that defines custom additions to a [ThemeData] object.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=8-szcYzFVao}
@ -241,6 +272,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
Iterable<Adaptation<Object>>? adaptations,
bool? applyElevationOverlayColor,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
Iterable<ThemeExtension<dynamic>>? extensions,
@ -366,6 +398,7 @@ class ThemeData with Diagnosticable {
// GENERAL CONFIGURATION
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
extensions ??= <ThemeExtension<dynamic>>[];
adaptations ??= <Adaptation<Object>>[];
inputDecorationTheme ??= const InputDecorationTheme();
platform ??= defaultTargetPlatform;
switch (platform) {
@ -551,6 +584,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptationMap: _createAdaptationMap(adaptations),
applyElevationOverlayColor: applyElevationOverlayColor,
cupertinoOverrideTheme: cupertinoOverrideTheme,
extensions: _themeExtensionIterableToMap(extensions),
@ -658,6 +692,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
required this.adaptationMap,
required this.applyElevationOverlayColor,
required this.cupertinoOverrideTheme,
required this.extensions,
@ -871,6 +906,19 @@ class ThemeData with Diagnosticable {
/// text geometry.
factory ThemeData.fallback({bool? useMaterial3}) => ThemeData.light(useMaterial3: useMaterial3);
/// Used to obtain a particular [Adaptation] from [adaptationMap].
///
/// To get an adaptation, use `Theme.of(context).getAdaptation<MyAdaptation>()`.
Adaptation<T>? getAdaptation<T>() => adaptationMap[T] as Adaptation<T>?;
static Map<Type, Adaptation<Object>> _createAdaptationMap(Iterable<Adaptation<Object>> adaptations) {
final Map<Type, Adaptation<Object>> adaptationMap = <Type, Adaptation<Object>>{
for (final Adaptation<Object> adaptation in adaptations)
adaptation.type: adaptation
};
return adaptationMap;
}
/// The overall theme brightness.
///
/// The default [TextStyle] color for the [textTheme] is black if the
@ -960,6 +1008,12 @@ class ThemeData with Diagnosticable {
/// See [extensions] for an interactive example.
T? extension<T>() => extensions[T] as T?;
/// A map which contains the adaptations for the theme. The entry's key is the
/// type of the adaptation; the value is the adaptation itself.
///
/// To obtain an adaptation, use [getAdaptation].
final Map<Type, Adaptation<Object>> adaptationMap;
/// The default [InputDecoration] values for [InputDecorator], [TextField],
/// and [TextFormField] are based on this theme.
///
@ -1480,6 +1534,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
Iterable<Adaptation<Object>>? adaptations,
bool? applyElevationOverlayColor,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
Iterable<ThemeExtension<dynamic>>? extensions,
@ -1612,6 +1667,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptationMap: adaptations != null ? _createAdaptationMap(adaptations) : adaptationMap,
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
extensions: (extensions != null) ? _themeExtensionIterableToMap(extensions) : this.extensions,
@ -1812,6 +1868,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptationMap: t < 0.5 ? a.adaptationMap : b.adaptationMap,
applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
extensions: _lerpThemeExtensions(a, b, t),
@ -1917,6 +1974,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
mapEquals(other.adaptationMap, adaptationMap) &&
other.applyElevationOverlayColor == applyElevationOverlayColor &&
other.cupertinoOverrideTheme == cupertinoOverrideTheme &&
mapEquals(other.extensions, extensions) &&
@ -2018,6 +2076,8 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
...adaptationMap.keys,
...adaptationMap.values,
applyElevationOverlayColor,
cupertinoOverrideTheme,
...extensions.keys,
@ -2123,6 +2183,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
properties.add(IterableProperty<Adaptation<dynamic>>('adaptations', adaptationMap.values, defaultValue: defaultData.adaptationMap.values, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<bool>('applyElevationOverlayColor', applyElevationOverlayColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, level: DiagnosticLevel.debug));
properties.add(IterableProperty<ThemeExtension<dynamic>>('extensions', extensions.values, defaultValue: defaultData.extensions.values, level: DiagnosticLevel.debug));

View file

@ -150,6 +150,7 @@ void main() {
find.byType(Switch),
paints
..rrect(color: Colors.blue[500])
..rrect()
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -163,6 +164,7 @@ void main() {
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(color: Colors.green[500])
..rrect()
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -221,7 +223,7 @@ void main() {
);
});
testWidgetsWithLeakTracking('SwitchListTile.adaptive delegates to', (WidgetTester tester) async {
testWidgetsWithLeakTracking('SwitchListTile.adaptive only uses material switch', (WidgetTester tester) async {
bool value = false;
Widget buildFrame(TargetPlatform platform) {
@ -246,23 +248,15 @@ void main() {
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS,
TargetPlatform.macOS, TargetPlatform.android, TargetPlatform.fuchsia,
TargetPlatform.linux, TargetPlatform.windows ]) {
value = false;
await tester.pumpWidget(buildFrame(platform));
expect(find.byType(CupertinoSwitch), findsOneWidget);
expect(value, isFalse, reason: 'on ${platform.name}');
await tester.tap(find.byType(SwitchListTile));
expect(value, isTrue, reason: 'on ${platform.name}');
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
value = false;
await tester.pumpWidget(buildFrame(platform));
await tester.pumpAndSettle(); // Finish the theme change animation.
expect(find.byType(CupertinoSwitch), findsNothing);
expect(find.byType(Switch), findsOneWidget);
expect(value, isFalse, reason: 'on ${platform.name}');
await tester.tap(find.byType(SwitchListTile));
expect(value, isTrue, reason: 'on ${platform.name}');
}
@ -714,14 +708,14 @@ void main() {
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: inactiveDisabledThumbColor)
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: inactiveDisabledThumbColor)
);
await tester.pumpWidget(buildSwitchListTile(enabled: false, selected: true));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: activeDisabledThumbColor)
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: activeDisabledThumbColor)
);
await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: false));
@ -729,7 +723,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: inactiveEnabledThumbColor)
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: inactiveEnabledThumbColor)
);
await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: true));
@ -737,7 +731,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: activeEnabledThumbColor)
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: activeEnabledThumbColor)
);
});
@ -853,7 +847,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: hoveredThumbColor),
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: hoveredThumbColor),
);
// On pressed state
@ -861,7 +855,7 @@ void main() {
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect()..rrect()..rrect()..rrect()..rrect(color: pressedThumbColor),
paints..rrect()..rrect()..rrect()..rrect()..rrect()..rrect(color: pressedThumbColor),
);
});
@ -1188,7 +1182,7 @@ void main() {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildSwitchListTile(true, platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget);
expect(find.byType(Switch), findsOneWidget);
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF2196F3)),
@ -1196,7 +1190,7 @@ void main() {
await tester.pumpWidget(buildSwitchListTile(false, platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget);
expect(find.byType(Switch), findsOneWidget);
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF34C759)),
@ -1224,7 +1218,7 @@ void main() {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildSwitchListTile(true, platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget);
expect(find.byType(Switch), findsOneWidget);
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF6750A4)),
@ -1232,7 +1226,7 @@ void main() {
await tester.pumpWidget(buildSwitchListTile(false, platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget);
expect(find.byType(Switch), findsOneWidget);
expect(
Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF34C759)),

View file

@ -434,6 +434,7 @@ 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)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -450,6 +451,7 @@ void main() {
color: const Color(0x802196f3),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -523,6 +525,71 @@ void main() {
);
});
testWidgetsWithLeakTracking('Switch.adaptive(Cupertino) has default colors when enabled', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
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.adaptive(
dragStartBehavior: DragStartBehavior.down,
value: value,
onChanged: (bool newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
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)),
)
..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));
await tester.pump();
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)),
)
..rrect()
..rrect(color: colors.onPrimary), // thumb color
reason: 'Active enabled switch should match these colors',
);
});
testWidgetsWithLeakTracking('Material2 - Switch has default colors when disabled', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
@ -548,6 +615,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -579,6 +647,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -790,6 +859,7 @@ void main() {
color: Colors.blue[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -805,6 +875,7 @@ void main() {
color: Colors.green[500],
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1137,12 +1208,13 @@ void main() {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
value = false;
await tester.pumpWidget(buildFrame(platform));
expect(find.byType(CupertinoSwitch), findsOneWidget, reason: 'on ${platform.name}');
expect(find.byType(Switch), findsOneWidget, reason: 'on ${platform.name}');
expect(find.byType(CupertinoSwitch), findsNothing);
final CupertinoSwitch adaptiveSwitch = tester.widget(find.byType(CupertinoSwitch));
final Switch adaptiveSwitch = tester.widget(find.byType(Switch));
expect(adaptiveSwitch.activeColor, activeTrackColor, reason: 'on ${platform.name}');
expect(adaptiveSwitch.trackColor, inactiveTrackColor, reason: 'on ${platform.name}');
expect(adaptiveSwitch.thumbColor, thumbColor, reason: 'on ${platform.name}');
expect(adaptiveSwitch.inactiveTrackColor, inactiveTrackColor, reason: 'on ${platform.name}');
expect(adaptiveSwitch.thumbColor?.resolve(<MaterialState>{}), thumbColor, reason: 'on ${platform.name}');
expect(adaptiveSwitch.focusColor, focusColor, reason: 'on ${platform.name}');
expect(value, isFalse, reason: 'on ${platform.name}');
@ -1161,6 +1233,463 @@ void main() {
}
});
testWidgetsWithLeakTracking('Switch.adaptive default mouse cursor(Cupertino)', (WidgetTester tester) async {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildAdaptiveSwitch(
platform: platform,
value: false,
));
final Size switchSize = tester.getSize(find.byType(Switch));
expect(switchSize, const Size(60.0, 48.0));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Switch)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(Switch)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic);
await tester.pumpWidget(buildAdaptiveSwitch(platform: platform));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic);
// Test disabled switch.
await tester.pumpWidget(buildAdaptiveSwitch(platform: platform, enabled: false, value: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
await gesture.removePointer(location: tester.getCenter(find.byType(Switch)));
await tester.pump();
}
});
testWidgetsWithLeakTracking('Switch.adaptive default thumb/track color and size(Cupertino)', (WidgetTester tester) async {
const Color thumbColor = Colors.white;
const Color inactiveTrackColor = Color.fromARGB(40, 120, 120, 128); // Default inactive track color.
const Color activeTrackColor = Color.fromARGB(255, 52, 199, 89); // Default active track color.
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
// Switches have same sizes on both platform but they are more compact on macOS.
final RRect trackRRect = platform == TargetPlatform.iOS
? RRect.fromLTRBR(4.5, 8.5, 55.5, 39.5, const Radius.circular(15.5))
: RRect.fromLTRBR(4.5, 4.5, 55.5, 35.5, const Radius.circular(15.5));
final RRect inactiveThumbRRect = platform == TargetPlatform.iOS
? RRect.fromLTRBR(6.0, 10.0, 34.0, 38.0, const Radius.circular(14.0))
: RRect.fromLTRBR(6.0, 6.0, 34.0, 34.0, const Radius.circular(14.0));
final RRect activeThumbRRect = platform == TargetPlatform.iOS
? RRect.fromLTRBR(26.0, 10.0, 54.0, 38.0, const Radius.circular(14.0))
: RRect.fromLTRBR(26.0, 6.0, 54.0, 34.0, const Radius.circular(14.0));
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAdaptiveSwitch(
platform: platform,
value: false
));
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveTrackColor,
rrect: trackRRect,
) // Default cupertino inactive track color
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: thumbColor,
rrect: inactiveThumbRRect,
),
reason: 'Inactive enabled switch should have default track and thumb color',
);
expect(find.byType(Opacity), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity)).opacity, 1.0);
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAdaptiveSwitch(platform: platform));
await tester.pump();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: activeTrackColor,
rrect: trackRRect,
) // Default cupertino active track color
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: thumbColor,
rrect: activeThumbRRect,
),
reason: 'Active enabled switch should have default track and thumb color',
);
expect(find.byType(Opacity), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity)).opacity, 1.0);
// Test disabled switch.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAdaptiveSwitch(
platform: platform,
enabled: false,
value: false,
));
await tester.pump();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: inactiveTrackColor,
rrect: trackRRect,
) // Default cupertino inactive track color
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: thumbColor,
rrect: inactiveThumbRRect,
),
reason: 'Inactive disabled switch should have default track and thumb color',
);
expect(find.byType(Opacity), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity)).opacity, 0.5);
await tester.pumpWidget(Container());
await tester.pumpWidget(buildAdaptiveSwitch(
platform: platform,
enabled: false,
));
await tester.pump();
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: activeTrackColor,
rrect: trackRRect,
) // Default cupertino active track color
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: thumbColor,
rrect: activeThumbRRect,
),
reason: 'Active disabled switch should have default track and thumb color',
);
expect(find.byType(Opacity), findsOneWidget);
expect(tester.widget<Opacity>(find.byType(Opacity)).opacity, 0.5);
}
});
testWidgetsWithLeakTracking('Default Switch.adaptive are not affected by '
'ThemeData.switchThemeData on iOS/macOS', (WidgetTester tester) async {
const Color defaultThumbColor = Colors.white;
const Color defaultInactiveTrackColor = Color.fromARGB(40, 120, 120, 128);
const Color defaultActiveTrackColor = Color.fromARGB(255, 52, 199, 89);
const Color updatedThumbColor = Colors.red;
const Color updatedTrackColor = Colors.green;
const SwitchThemeData overallSwitchTheme = SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(updatedThumbColor),
trackColor: MaterialStatePropertyAll<Color>(updatedTrackColor),
);
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
overallSwitchThemeData: overallSwitchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultActiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Active enabled switch should still have default track and thumb color',
);
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
value: false,
overallSwitchThemeData: overallSwitchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultInactiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Inactive enabled switch should have default track and thumb color',
);
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
enabled: false,
value: false,
overallSwitchThemeData: overallSwitchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultInactiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Inactive disabled switch should have default track and thumb color',
);
}
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: TargetPlatform.android,
overallSwitchThemeData: overallSwitchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Color(updatedTrackColor.value),
)
..rrect()
..rrect(
color: Color(updatedThumbColor.value),
),
reason: 'Switch.adaptive is affected by SwitchTheme on other platforms',
);
});
testWidgetsWithLeakTracking('Default Switch.adaptive are not affected by '
'SwitchThemeData on iOS/macOS', (WidgetTester tester) async {
const Color defaultThumbColor = Colors.white;
const Color defaultInactiveTrackColor = Color.fromARGB(40, 120, 120, 128);
const Color defaultActiveTrackColor = Color.fromARGB(255, 52, 199, 89);
const Color updatedThumbColor = Colors.red;
const Color updatedTrackColor = Colors.green;
const SwitchThemeData switchTheme = SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(updatedThumbColor),
trackColor: MaterialStatePropertyAll<Color>(updatedTrackColor),
);
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
switchThemeData: switchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultActiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Active enabled switch should still have default track and thumb color',
);
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
value: false,
switchThemeData: switchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultInactiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Inactive enabled switch should have default track and thumb color',
);
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
enabled: false,
value: false,
switchThemeData: switchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: defaultInactiveTrackColor,
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x26000000))
..rrect(color: const Color(0x0f000000))
..rrect(color: const Color(0x0a000000)) // Thumb border color(only cupertino)
..rrect(
color: defaultThumbColor,
),
reason: 'Inactive disabled switch should have default track and thumb color',
);
}
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: TargetPlatform.android,
switchThemeData: switchTheme
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Color(updatedTrackColor.value),
)
..rrect()
..rrect(
color: Color(updatedThumbColor.value),
),
reason: 'Switch.adaptive is affected by SwitchTheme on other platforms',
);
});
testWidgetsWithLeakTracking('Override default adaptive SwitchThemeData on iOS/macOS', (WidgetTester tester) async {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
switchThemeData: const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(Colors.yellow),
trackColor: MaterialStatePropertyAll<Color>(Colors.brown),
),
switchThemeAdaptation: const _SwitchThemeAdaptation(),
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Color(Colors.deepPurple.value),
)..rrect()..rrect()..rrect()..rrect()
..rrect(
color: Color(Colors.lightGreen.value),
),
);
}
// Other platforms should not be affected by the adaptive switch theme.
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(Container());
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: platform,
switchThemeData: const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(Colors.yellow),
trackColor: MaterialStatePropertyAll<Color>(Colors.brown),
),
switchThemeAdaptation: const _SwitchThemeAdaptation(),
)
);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(
color: Color(Colors.brown.value),
)..rrect()
..rrect(
color: Color(Colors.yellow.value),
),
);
}
});
testWidgetsWithLeakTracking('Switch.adaptive default focus color(Cupertino)', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final FocusNode node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: TargetPlatform.macOS,
autofocus: true,
focusNode: node,
)
);
await tester.pumpAndSettle();
expect(node.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(color: const Color(0xff34c759)) // Track color
..rrect()
..rrect(color: const Color(0xcc6ef28f), strokeWidth: 3.5, style: PaintingStyle.stroke) // Focused outline
..rrect()
..rrect()
..rrect()
..rrect(color: const Color(0xffffffff)), // Thumb color
);
await tester.pumpWidget(
buildAdaptiveSwitch(
platform: TargetPlatform.macOS,
autofocus: true,
focusNode: node,
focusColor: Colors.red,
)
);
await tester.pumpAndSettle();
expect(node.hasPrimaryFocus, isTrue);
expect(
Material.of(tester.element(find.byType(Switch))),
paints
..rrect(color: const Color(0xff34c759)) // Track color
..rrect()
..rrect(color: Color(Colors.red.value), strokeWidth: 3.5, style: PaintingStyle.stroke) // Focused outline
..rrect()..rrect()..rrect()
..rrect(color: const Color(0xffffffff)), // Thumb color
);
});
testWidgetsWithLeakTracking('Material2 - Switch is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
@ -1236,6 +1765,7 @@ void main() {
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1393,6 +1923,7 @@ void main() {
color: const Color(0x802196f3),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1430,6 +1961,7 @@ void main() {
color: const Color(0x1f000000),
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1750,6 +2282,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1767,6 +2300,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1784,6 +2318,7 @@ 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)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -1801,6 +2336,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -2442,6 +2978,7 @@ void main() {
color: Colors.black12,
rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
)
..rrect(color: const Color(0x00000000))
..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000))
@ -3428,6 +3965,7 @@ void main() {
testWidgetsWithLeakTracking('Switch.adaptive(Cupertino) is focusable and has correct focus color', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Switch.adaptive');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool value = true;
const Color focusColor = Color(0xffff0000);
@ -3462,9 +4000,10 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
find.byType(CupertinoSwitch),
find.byType(Switch),
paints
..rrect(color: const Color(0xff34c759))
..rrect(color: const Color(0x00000000))
..rrect(color: focusColor)
..clipRRect()
..rrect(color: const Color(0x26000000))
@ -3480,9 +4019,10 @@ void main() {
expect(focusNode.hasPrimaryFocus, isTrue);
expect(
find.byType(CupertinoSwitch),
find.byType(Switch),
paints
..rrect(color: const Color(0x28787880))
..rrect(color: const Color(0x00000000))
..rrect(color: focusColor)
..clipRRect()
..rrect(color: const Color(0x26000000))
@ -3498,7 +4038,7 @@ void main() {
expect(focusNode.hasPrimaryFocus, isFalse);
expect(
find.byType(CupertinoSwitch),
find.byType(Switch),
paints
..rrect(color: const Color(0x28787880))
..clipRRect()
@ -3507,8 +4047,6 @@ void main() {
..rrect(color: const Color(0x0a000000))
..rrect(color: const Color(0xffffffff)),
);
focusNode.dispose();
});
testWidgetsWithLeakTracking('Switch.onFocusChange callback', (WidgetTester tester) async {
@ -3608,3 +4146,68 @@ class _TestImageProvider extends ImageProvider<Object> {
@override
String toString() => '${describeIdentity(this)}()';
}
Widget buildAdaptiveSwitch({
required TargetPlatform platform,
bool enabled = true,
bool value = true,
bool autofocus = false,
FocusNode? focusNode,
Color? focusColor,
SwitchThemeData? overallSwitchThemeData,
SwitchThemeData? switchThemeData,
Adaptation<SwitchThemeData>? switchThemeAdaptation,
}) {
final Widget adaptiveSwitch = Switch.adaptive(
focusNode: focusNode,
autofocus: autofocus,
focusColor: focusColor,
value: value,
onChanged: enabled ? (_) {} : null,
);
return MaterialApp(
theme: ThemeData(
platform: platform,
switchTheme: overallSwitchThemeData,
adaptations: switchThemeAdaptation == null ? null : <Adaptation<Object>>[
switchThemeAdaptation
],
),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: switchThemeData == null
? adaptiveSwitch
: SwitchTheme(
data: switchThemeData,
child: adaptiveSwitch,
),
),
);
},
),
);
}
class _SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
const _SwitchThemeAdaptation();
@override
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) {
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.windows:
case TargetPlatform.linux:
return defaultValue;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(Colors.lightGreen),
trackColor: MaterialStatePropertyAll<Color>(Colors.deepPurple),
);
}
}
}

View file

@ -717,6 +717,7 @@ void main() {
..rrect()
..rrect()
..rrect()
..rrect()
..rrect(color: defaultThumbColor)
);
@ -730,6 +731,7 @@ void main() {
..rrect()
..rrect()
..rrect()
..rrect()
..rrect(color: selectedThumbColor)
);
});

View file

@ -724,6 +724,7 @@ void main() {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptationMap: const <Type, Adaptation<Object>>{},
applyElevationOverlayColor: false,
cupertinoOverrideTheme: null,
extensions: const <Object, ThemeExtension<dynamic>>{},
@ -836,6 +837,9 @@ void main() {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptationMap: const <Type, Adaptation<Object>>{
SwitchThemeData: SwitchThemeAdaptation(),
},
applyElevationOverlayColor: true,
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
extensions: const <Object, ThemeExtension<dynamic>>{
@ -941,6 +945,7 @@ void main() {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
adaptations: otherTheme.adaptationMap.values,
applyElevationOverlayColor: otherTheme.applyElevationOverlayColor,
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
extensions: otherTheme.extensions.values,
@ -1041,6 +1046,7 @@ void main() {
// alphabetical by symbol name.
// GENERAL CONFIGURATION
expect(themeDataCopy.adaptationMap, equals(otherTheme.adaptationMap));
expect(themeDataCopy.applyElevationOverlayColor, equals(otherTheme.applyElevationOverlayColor));
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));
expect(themeDataCopy.extensions, equals(otherTheme.extensions));
@ -1178,6 +1184,7 @@ void main() {
// List of properties must match the properties in ThemeData.hashCode()
final Set<String> expectedPropertyNames = <String>{
// GENERAL CONFIGURATION
'adaptations',
'applyElevationOverlayColor',
'cupertinoOverrideTheme',
'extensions',
@ -1285,100 +1292,139 @@ void main() {
expect(propertyNames, expectedPropertyNames);
});
group('Theme adaptationMap', () {
const Key containerKey = Key('container');
testWidgetsWithLeakTracking('can be obtained', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
adaptations: const <Adaptation<Object>>[
StringAdaptation(),
SwitchThemeAdaptation()
],
),
home: Container(key: containerKey),
),
);
final ThemeData theme = Theme.of(
tester.element(find.byKey(containerKey)),
);
final String adaptiveString = theme.getAdaptation<String>()!.adapt(theme, 'Default theme');
final SwitchThemeData adaptiveSwitchTheme = theme.getAdaptation<SwitchThemeData>()!
.adapt(theme, theme.switchTheme);
expect(adaptiveString, 'Adaptive theme.');
expect(adaptiveSwitchTheme.thumbColor?.resolve(<MaterialState>{}),
isSameColorAs(Colors.brown));
});
testWidgetsWithLeakTracking('should return null on extension not found', (WidgetTester tester) async {
final ThemeData theme = ThemeData(
adaptations: const <Adaptation<Object>>[
StringAdaptation(),
],
);
expect(theme.extension<SwitchThemeAdaptation>(), isNull);
});
});
testWidgetsWithLeakTracking(
'ThemeData.brightness not matching ColorScheme.brightness throws a helpful error message', (WidgetTester tester) async {
AssertionError? error;
AssertionError? error;
// Test `ColorScheme.light()` and `ThemeData.brightness == Brightness.dark`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
// Test `ColorScheme.light()` and `ThemeData.brightness == Brightness.dark`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
brightness: Brightness.dark,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.dark()` and `ThemeData.brightness == Brightness.light`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.dark(),
brightness: Brightness.light,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.fromSeed()` and `ThemeData.brightness == Brightness.dark`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xffff0000)),
brightness: Brightness.dark,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.fromSeed()` using `Brightness.dark` and `ThemeData.brightness == Brightness.light`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xffff0000),
brightness: Brightness.dark,
),
home: const Placeholder(),
brightness: Brightness.light,
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.dark()` and `ThemeData.brightness == Brightness.light`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.dark(),
brightness: Brightness.light,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.fromSeed()` and `ThemeData.brightness == Brightness.dark`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xffff0000)),
brightness: Brightness.dark,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
// Test `ColorScheme.fromSeed()` using `Brightness.dark` and `ThemeData.brightness == Brightness.light`.
try {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xffff0000),
brightness: Brightness.dark,
),
brightness: Brightness.light,
),
home: const Placeholder(),
),
);
} on AssertionError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.'
));
}
));
}
});
}
@ -1437,3 +1483,19 @@ class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
);
}
}
class SwitchThemeAdaptation extends Adaptation<SwitchThemeData> {
const SwitchThemeAdaptation();
@override
SwitchThemeData adapt(ThemeData theme, SwitchThemeData defaultValue) => const SwitchThemeData(
thumbColor: MaterialStatePropertyAll<Color>(Colors.brown),
);
}
class StringAdaptation extends Adaptation<String> {
const StringAdaptation();
@override
String adapt(ThemeData theme, String defaultValue) => 'Adaptive theme.';
}