mirror of
https://github.com/flutter/flutter
synced 2024-10-06 00:09:53 +00:00
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:
parent
a76720e9f6
commit
ed70f4e248
|
@ -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')});
|
||||
|
||||
|
|
134
examples/api/lib/material/switch/switch.4.dart
Normal file
134
examples/api/lib/material/switch/switch.4.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
63
examples/api/test/material/switch/switch.4_test.dart
Normal file
63
examples/api/test/material/switch/switch.4_test.dart
Normal 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
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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.';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue