Unverified Commit ed70f4e2 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

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.
parent a76720e9
...@@ -126,6 +126,12 @@ class _${blockName}DefaultsM3 extends SwitchThemeData { ...@@ -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 @override
MaterialStatePropertyAll<double> get trackOutlineWidth => const MaterialStatePropertyAll<double>(${getToken('md.comp.switch.track.outline.width')}); MaterialStatePropertyAll<double> get trackOutlineWidth => const MaterialStatePropertyAll<double>(${getToken('md.comp.switch.track.outline.width')});
......
// 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),
);
}
}
}
// 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
);
});
}
...@@ -73,6 +73,37 @@ export 'package:flutter/services.dart' show Brightness; ...@@ -73,6 +73,37 @@ export 'package:flutter/services.dart' show Brightness;
// Examples can assume: // Examples can assume:
// late BuildContext context; // 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. /// An interface that defines custom additions to a [ThemeData] object.
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=8-szcYzFVao} /// {@youtube 560 315 https://www.youtube.com/watch?v=8-szcYzFVao}
...@@ -241,6 +272,7 @@ class ThemeData with Diagnosticable { ...@@ -241,6 +272,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
Iterable<Adaptation<Object>>? adaptations,
bool? applyElevationOverlayColor, bool? applyElevationOverlayColor,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme, NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
Iterable<ThemeExtension<dynamic>>? extensions, Iterable<ThemeExtension<dynamic>>? extensions,
...@@ -366,6 +398,7 @@ class ThemeData with Diagnosticable { ...@@ -366,6 +398,7 @@ class ThemeData with Diagnosticable {
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault();
extensions ??= <ThemeExtension<dynamic>>[]; extensions ??= <ThemeExtension<dynamic>>[];
adaptations ??= <Adaptation<Object>>[];
inputDecorationTheme ??= const InputDecorationTheme(); inputDecorationTheme ??= const InputDecorationTheme();
platform ??= defaultTargetPlatform; platform ??= defaultTargetPlatform;
switch (platform) { switch (platform) {
...@@ -551,6 +584,7 @@ class ThemeData with Diagnosticable { ...@@ -551,6 +584,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptationMap: _createAdaptationMap(adaptations),
applyElevationOverlayColor: applyElevationOverlayColor, applyElevationOverlayColor: applyElevationOverlayColor,
cupertinoOverrideTheme: cupertinoOverrideTheme, cupertinoOverrideTheme: cupertinoOverrideTheme,
extensions: _themeExtensionIterableToMap(extensions), extensions: _themeExtensionIterableToMap(extensions),
...@@ -658,6 +692,7 @@ class ThemeData with Diagnosticable { ...@@ -658,6 +692,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
required this.adaptationMap,
required this.applyElevationOverlayColor, required this.applyElevationOverlayColor,
required this.cupertinoOverrideTheme, required this.cupertinoOverrideTheme,
required this.extensions, required this.extensions,
...@@ -871,6 +906,19 @@ class ThemeData with Diagnosticable { ...@@ -871,6 +906,19 @@ class ThemeData with Diagnosticable {
/// text geometry. /// text geometry.
factory ThemeData.fallback({bool? useMaterial3}) => ThemeData.light(useMaterial3: useMaterial3); 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 overall theme brightness.
/// ///
/// The default [TextStyle] color for the [textTheme] is black if the /// The default [TextStyle] color for the [textTheme] is black if the
...@@ -960,6 +1008,12 @@ class ThemeData with Diagnosticable { ...@@ -960,6 +1008,12 @@ class ThemeData with Diagnosticable {
/// See [extensions] for an interactive example. /// See [extensions] for an interactive example.
T? extension<T>() => extensions[T] as T?; 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], /// The default [InputDecoration] values for [InputDecorator], [TextField],
/// and [TextFormField] are based on this theme. /// and [TextFormField] are based on this theme.
/// ///
...@@ -1480,6 +1534,7 @@ class ThemeData with Diagnosticable { ...@@ -1480,6 +1534,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
Iterable<Adaptation<Object>>? adaptations,
bool? applyElevationOverlayColor, bool? applyElevationOverlayColor,
NoDefaultCupertinoThemeData? cupertinoOverrideTheme, NoDefaultCupertinoThemeData? cupertinoOverrideTheme,
Iterable<ThemeExtension<dynamic>>? extensions, Iterable<ThemeExtension<dynamic>>? extensions,
...@@ -1612,6 +1667,7 @@ class ThemeData with Diagnosticable { ...@@ -1612,6 +1667,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptationMap: adaptations != null ? _createAdaptationMap(adaptations) : adaptationMap,
applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor, applyElevationOverlayColor: applyElevationOverlayColor ?? this.applyElevationOverlayColor,
cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme, cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme,
extensions: (extensions != null) ? _themeExtensionIterableToMap(extensions) : this.extensions, extensions: (extensions != null) ? _themeExtensionIterableToMap(extensions) : this.extensions,
...@@ -1812,6 +1868,7 @@ class ThemeData with Diagnosticable { ...@@ -1812,6 +1868,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptationMap: t < 0.5 ? a.adaptationMap : b.adaptationMap,
applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor, applyElevationOverlayColor:t < 0.5 ? a.applyElevationOverlayColor : b.applyElevationOverlayColor,
cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme, cupertinoOverrideTheme:t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme,
extensions: _lerpThemeExtensions(a, b, t), extensions: _lerpThemeExtensions(a, b, t),
...@@ -1917,6 +1974,7 @@ class ThemeData with Diagnosticable { ...@@ -1917,6 +1974,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
mapEquals(other.adaptationMap, adaptationMap) &&
other.applyElevationOverlayColor == applyElevationOverlayColor && other.applyElevationOverlayColor == applyElevationOverlayColor &&
other.cupertinoOverrideTheme == cupertinoOverrideTheme && other.cupertinoOverrideTheme == cupertinoOverrideTheme &&
mapEquals(other.extensions, extensions) && mapEquals(other.extensions, extensions) &&
...@@ -2018,6 +2076,8 @@ class ThemeData with Diagnosticable { ...@@ -2018,6 +2076,8 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
...adaptationMap.keys,
...adaptationMap.values,
applyElevationOverlayColor, applyElevationOverlayColor,
cupertinoOverrideTheme, cupertinoOverrideTheme,
...extensions.keys, ...extensions.keys,
...@@ -2123,6 +2183,7 @@ class ThemeData with Diagnosticable { ...@@ -2123,6 +2183,7 @@ class ThemeData with Diagnosticable {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // 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<bool>('applyElevationOverlayColor', applyElevationOverlayColor, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NoDefaultCupertinoThemeData>('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme, 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)); properties.add(IterableProperty<ThemeExtension<dynamic>>('extensions', extensions.values, defaultValue: defaultData.extensions.values, level: DiagnosticLevel.debug));
......
...@@ -150,6 +150,7 @@ void main() { ...@@ -150,6 +150,7 @@ void main() {
find.byType(Switch), find.byType(Switch),
paints paints
..rrect(color: Colors.blue[500]) ..rrect(color: Colors.blue[500])
..rrect()
..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0x1f000000))
...@@ -163,6 +164,7 @@ void main() { ...@@ -163,6 +164,7 @@ void main() {
Material.of(tester.element(find.byType(Switch))), Material.of(tester.element(find.byType(Switch))),
paints paints
..rrect(color: Colors.green[500]) ..rrect(color: Colors.green[500])
..rrect()
..rrect(color: const Color(0x33000000)) ..rrect(color: const Color(0x33000000))
..rrect(color: const Color(0x24000000)) ..rrect(color: const Color(0x24000000))
..rrect(color: const Color(0x1f000000)) ..rrect(color: const Color(0x1f000000))
...@@ -221,7 +223,7 @@ void main() { ...@@ -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; bool value = false;
Widget buildFrame(TargetPlatform platform) { Widget buildFrame(TargetPlatform platform) {
...@@ -246,23 +248,15 @@ void main() { ...@@ -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; value = false;
await tester.pumpWidget(buildFrame(platform)); 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(CupertinoSwitch), findsNothing);
expect(find.byType(Switch), findsOneWidget);
expect(value, isFalse, reason: 'on ${platform.name}'); expect(value, isFalse, reason: 'on ${platform.name}');
await tester.tap(find.byType(SwitchListTile)); await tester.tap(find.byType(SwitchListTile));
expect(value, isTrue, reason: 'on ${platform.name}'); expect(value, isTrue, reason: 'on ${platform.name}');
} }
...@@ -714,14 +708,14 @@ void main() { ...@@ -714,14 +708,14 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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.pumpWidget(buildSwitchListTile(enabled: false, selected: true));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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)); await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: false));
...@@ -729,7 +723,7 @@ void main() { ...@@ -729,7 +723,7 @@ void main() {
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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)); await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: true));
...@@ -737,7 +731,7 @@ void main() { ...@@ -737,7 +731,7 @@ void main() {
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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() { ...@@ -853,7 +847,7 @@ void main() {
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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 // On pressed state
...@@ -861,7 +855,7 @@ void main() { ...@@ -861,7 +855,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(
Material.of(tester.element(find.byType(Switch))), 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() { ...@@ -1188,7 +1182,7 @@ void main() {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) { for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildSwitchListTile(true, platform)); await tester.pumpWidget(buildSwitchListTile(true, platform));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget); expect(find.byType(Switch), findsOneWidget);
expect( expect(
Material.of(tester.element(find.byType(Switch))), Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF2196F3)), paints..rrect(color: const Color(0xFF2196F3)),
...@@ -1196,7 +1190,7 @@ void main() { ...@@ -1196,7 +1190,7 @@ void main() {
await tester.pumpWidget(buildSwitchListTile(false, platform)); await tester.pumpWidget(buildSwitchListTile(false, platform));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget); expect(find.byType(Switch), findsOneWidget);
expect( expect(
Material.of(tester.element(find.byType(Switch))), Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF34C759)), paints..rrect(color: const Color(0xFF34C759)),
...@@ -1224,7 +1218,7 @@ void main() { ...@@ -1224,7 +1218,7 @@ void main() {
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) { for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildSwitchListTile(true, platform)); await tester.pumpWidget(buildSwitchListTile(true, platform));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget); expect(find.byType(Switch), findsOneWidget);
expect( expect(
Material.of(tester.element(find.byType(Switch))), Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF6750A4)), paints..rrect(color: const Color(0xFF6750A4)),
...@@ -1232,7 +1226,7 @@ void main() { ...@@ -1232,7 +1226,7 @@ void main() {
await tester.pumpWidget(buildSwitchListTile(false, platform)); await tester.pumpWidget(buildSwitchListTile(false, platform));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(CupertinoSwitch), findsOneWidget); expect(find.byType(Switch), findsOneWidget);
expect( expect(
Material.of(tester.element(find.byType(Switch))), Material.of(tester.element(find.byType(Switch))),
paints..rrect(color: const Color(0xFF34C759)), paints..rrect(color: const Color(0xFF34C759)),
......
...@@ -717,6 +717,7 @@ void main() { ...@@ -717,6 +717,7 @@ void main() {
..rrect() ..rrect()
..rrect() ..rrect()
..rrect() ..rrect()
..rrect()
..rrect(color: defaultThumbColor) ..rrect(color: defaultThumbColor)
); );
...@@ -730,6 +731,7 @@ void main() { ...@@ -730,6 +731,7 @@ void main() {
..rrect() ..rrect()
..rrect() ..rrect()
..rrect() ..rrect()
..rrect()
..rrect(color: selectedThumbColor) ..rrect(color: selectedThumbColor)
); );
}); });
......
...@@ -724,6 +724,7 @@ void main() { ...@@ -724,6 +724,7 @@ void main() {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptationMap: const <Type, Adaptation<Object>>{},
applyElevationOverlayColor: false, applyElevationOverlayColor: false,
cupertinoOverrideTheme: null, cupertinoOverrideTheme: null,
extensions: const <Object, ThemeExtension<dynamic>>{}, extensions: const <Object, ThemeExtension<dynamic>>{},
...@@ -836,6 +837,9 @@ void main() { ...@@ -836,6 +837,9 @@ void main() {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptationMap: const <Type, Adaptation<Object>>{
SwitchThemeData: SwitchThemeAdaptation(),
},
applyElevationOverlayColor: true, applyElevationOverlayColor: true,
cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme, cupertinoOverrideTheme: ThemeData.light().cupertinoOverrideTheme,
extensions: const <Object, ThemeExtension<dynamic>>{ extensions: const <Object, ThemeExtension<dynamic>>{
...@@ -941,6 +945,7 @@ void main() { ...@@ -941,6 +945,7 @@ void main() {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
adaptations: otherTheme.adaptationMap.values,
applyElevationOverlayColor: otherTheme.applyElevationOverlayColor, applyElevationOverlayColor: otherTheme.applyElevationOverlayColor,
cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme, cupertinoOverrideTheme: otherTheme.cupertinoOverrideTheme,
extensions: otherTheme.extensions.values, extensions: otherTheme.extensions.values,
...@@ -1041,6 +1046,7 @@ void main() { ...@@ -1041,6 +1046,7 @@ void main() {
// alphabetical by symbol name. // alphabetical by symbol name.
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
expect(themeDataCopy.adaptationMap, equals(otherTheme.adaptationMap));
expect(themeDataCopy.applyElevationOverlayColor, equals(otherTheme.applyElevationOverlayColor)); expect(themeDataCopy.applyElevationOverlayColor, equals(otherTheme.applyElevationOverlayColor));
expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme)); expect(themeDataCopy.cupertinoOverrideTheme, equals(otherTheme.cupertinoOverrideTheme));
expect(themeDataCopy.extensions, equals(otherTheme.extensions)); expect(themeDataCopy.extensions, equals(otherTheme.extensions));
...@@ -1178,6 +1184,7 @@ void main() { ...@@ -1178,6 +1184,7 @@ void main() {
// List of properties must match the properties in ThemeData.hashCode() // List of properties must match the properties in ThemeData.hashCode()
final Set<String> expectedPropertyNames = <String>{ final Set<String> expectedPropertyNames = <String>{
// GENERAL CONFIGURATION // GENERAL CONFIGURATION
'adaptations',
'applyElevationOverlayColor', 'applyElevationOverlayColor',
'cupertinoOverrideTheme', 'cupertinoOverrideTheme',
'extensions', 'extensions',
...@@ -1285,100 +1292,139 @@ void main() { ...@@ -1285,100 +1292,139 @@ void main() {
expect(propertyNames, expectedPropertyNames); 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( testWidgetsWithLeakTracking(
'ThemeData.brightness not matching ColorScheme.brightness throws a helpful error message', (WidgetTester tester) async { '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`. // Test `ColorScheme.light()` and `ThemeData.brightness == Brightness.dark`.
try { try {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
colorScheme: const ColorScheme.light(), colorScheme: const ColorScheme.light(),
brightness: Brightness.dark, brightness: Brightness.dark,
),
home: const Placeholder(),
), ),
); home: const Placeholder(),
} on AssertionError catch (e) { ),
error = e; );
} finally { } on AssertionError catch (e) {
expect(error, isNotNull); error = e;
expect(error?.message, contains( } finally {
'ThemeData.brightness does not match ColorScheme.brightness. ' expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to ' 'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.' 'match the other.'
)); ));
} }
// Test `ColorScheme.dark()` and `ThemeData.brightness == Brightness.light`. // Test `ColorScheme.dark()` and `ThemeData.brightness == Brightness.light`.
try { try {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
colorScheme: const ColorScheme.dark(), colorScheme: const ColorScheme.dark(),
brightness: Brightness.light, brightness: Brightness.light,
),
home: const Placeholder(),
), ),
); home: const Placeholder(),
} on AssertionError catch (e) { ),
error = e; );
} finally { } on AssertionError catch (e) {
expect(error, isNotNull); error = e;
expect(error?.message, contains( } finally {
'ThemeData.brightness does not match ColorScheme.brightness. ' expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to ' 'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.' 'match the other.'
)); ));
} }
// Test `ColorScheme.fromSeed()` and `ThemeData.brightness == Brightness.dark`. // Test `ColorScheme.fromSeed()` and `ThemeData.brightness == Brightness.dark`.
try { try {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xffff0000)), colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xffff0000)),
brightness: Brightness.dark, brightness: Brightness.dark,
),
home: const Placeholder(),
), ),
); home: const Placeholder(),
} on AssertionError catch (e) { ),
error = e; );
} finally { } on AssertionError catch (e) {
expect(error, isNotNull); error = e;
expect(error?.message, contains( } finally {
'ThemeData.brightness does not match ColorScheme.brightness. ' expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to ' 'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.' 'match the other.'
)); ));
} }
// Test `ColorScheme.fromSeed()` using `Brightness.dark` and `ThemeData.brightness == Brightness.light`. // Test `ColorScheme.fromSeed()` using `Brightness.dark` and `ThemeData.brightness == Brightness.light`.
try { try {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xffff0000), seedColor: const Color(0xffff0000),
brightness: Brightness.dark, brightness: Brightness.dark,
),
brightness: Brightness.light,
), ),
home: const Placeholder(), brightness: Brightness.light,
), ),
); home: const Placeholder(),
} on AssertionError catch (e) { ),
error = e; );
} finally { } on AssertionError catch (e) {
expect(error, isNotNull); error = e;
expect(error?.message, contains( } finally {
'ThemeData.brightness does not match ColorScheme.brightness. ' expect(error, isNotNull);
expect(error?.message, contains(
'ThemeData.brightness does not match ColorScheme.brightness. '
'Either override ColorScheme.brightness or ThemeData.brightness to ' 'Either override ColorScheme.brightness or ThemeData.brightness to '
'match the other.' 'match the other.'
)); ));
} }
}); });
} }
...@@ -1437,3 +1483,19 @@ class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> { ...@@ -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.';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment