From 1e696d304c0a4c3bcec748ffb1e31fc4d20dc1c7 Mon Sep 17 00:00:00 2001 From: Pierre-Louis <6655696+guidezpl@users.noreply.github.com> Date: Tue, 6 Dec 2022 15:32:58 +0100 Subject: [PATCH] Support theming `CupertinoSwitch`s (#116510) * Introduce flag to maximally apply CupertinoTheme * add missing docs * add tests * fix docs * fix test --- .../api/lib/material/switch/switch.3.dart | 68 +++++++++++++++++++ .../test/material/switch/switch.1_test.dart | 2 +- .../test/material/switch/switch.3_test.dart | 24 +++++++ .../flutter/lib/src/cupertino/switch.dart | 27 ++++++-- packages/flutter/lib/src/cupertino/theme.dart | 38 +++++++++++ packages/flutter/lib/src/material/switch.dart | 6 ++ .../flutter/lib/src/material/theme_data.dart | 3 + .../flutter/test/cupertino/switch_test.dart | 60 ++++++++++++++++ .../flutter/test/cupertino/theme_test.dart | 6 ++ 9 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 examples/api/lib/material/switch/switch.3.dart create mode 100644 examples/api/test/material/switch/switch.3_test.dart diff --git a/examples/api/lib/material/switch/switch.3.dart b/examples/api/lib/material/switch/switch.3.dart new file mode 100644 index 000000000000..7caed5719c82 --- /dev/null +++ b/examples/api/lib/material/switch/switch.3.dart @@ -0,0 +1,68 @@ +// 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. + +/// Flutter code sample for [Switch]. +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +void main() => runApp(const SwitchApp()); + +class SwitchApp extends StatelessWidget { + const SwitchApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData.light(useMaterial3: true).copyWith( + // Use the ambient [CupetinoThemeData] to style all widgets which would + // otherwise use iOS defaults. + cupertinoOverrideTheme: const CupertinoThemeData(applyThemeToAll: true), + ), + home: Scaffold( + appBar: AppBar(title: const Text('Switch Sample')), + body: const Center( + child: SwitchExample(), + ), + ), + ); + } +} + +class SwitchExample extends StatefulWidget { + const SwitchExample({super.key}); + + @override + State createState() => _SwitchExampleState(); +} + +class _SwitchExampleState extends State { + bool light = true; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Switch.adaptive( + value: light, + onChanged: (bool value) { + setState(() { + light = value; + }); + }, + ), + Switch.adaptive( + // Don't use the ambient [CupetinoThemeData] to style this switch. + applyCupertinoTheme: false, + value: light, + onChanged: (bool value) { + setState(() { + light = value; + }); + }, + ), + ], + ); + } +} diff --git a/examples/api/test/material/switch/switch.1_test.dart b/examples/api/test/material/switch/switch.1_test.dart index eb972b265dd6..54f4a8bc7f33 100644 --- a/examples/api/test/material/switch/switch.1_test.dart +++ b/examples/api/test/material/switch/switch.1_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter_api_samples/material/switch/switch.0.dart' as example; +import 'package:flutter_api_samples/material/switch/switch.1.dart' as example; import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/examples/api/test/material/switch/switch.3_test.dart b/examples/api/test/material/switch/switch.3_test.dart new file mode 100644 index 000000000000..bb40f9c1ec05 --- /dev/null +++ b/examples/api/test/material/switch/switch.3_test.dart @@ -0,0 +1,24 @@ +// 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.3.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Can toggle switch', (WidgetTester tester) async { + await tester.pumpWidget( + const example.SwitchApp(), + ); + + final Finder switchFinder = find.byType(Switch).first; + Switch materialSwitch = tester.widget(switchFinder); + expect(materialSwitch.value, true); + + await tester.tap(switchFinder); + await tester.pumpAndSettle(); + materialSwitch = tester.widget(switchFinder); + expect(materialSwitch.value, false); + }); +} diff --git a/packages/flutter/lib/src/cupertino/switch.dart b/packages/flutter/lib/src/cupertino/switch.dart index 1433289d4d33..26d86ce8b842 100644 --- a/packages/flutter/lib/src/cupertino/switch.dart +++ b/packages/flutter/lib/src/cupertino/switch.dart @@ -14,6 +14,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'theme.dart'; import 'thumb_painter.dart'; // Examples can assume: @@ -72,6 +73,7 @@ class CupertinoSwitch extends StatefulWidget { this.activeColor, this.trackColor, this.thumbColor, + this.applyTheme, this.dragStartBehavior = DragStartBehavior.start, }) : assert(value != null), assert(dragStartBehavior != null); @@ -105,13 +107,15 @@ class CupertinoSwitch extends StatefulWidget { /// ``` final ValueChanged? onChanged; - /// The color to use when this switch is on. + /// The color to use for the track when the switch is on. /// - /// Defaults to [CupertinoColors.systemGreen] when null and ignores - /// the [CupertinoTheme] in accordance to native iOS behavior. + /// If null and [applyTheme] is false, defaults to [CupertinoColors.systemGreen] + /// in accordance to native iOS behavior. Otherwise, defaults to + /// [CupertinoThemeData.primaryColor]. final Color? activeColor; - /// The color to use for the background when the switch is off. + + /// The color to use for the track when the switch is off. /// /// Defaults to [CupertinoColors.secondarySystemFill] when null. final Color? trackColor; @@ -121,6 +125,16 @@ class CupertinoSwitch extends StatefulWidget { /// Defaults to [CupertinoColors.white] when null. final Color? thumbColor; + /// {@template flutter.cupertino.CupertinoSwitch.applyTheme} + /// Whether to apply the ambient [CupertinoThemeData]. + /// + /// If true, the track uses [CupertinoThemeData.primaryColor] for the track + /// when the switch is on. + /// + /// Defaults to [CupertinoThemeData.applyThemeToAll]. + /// {@endtemplate} + final bool? applyTheme; + /// {@template flutter.cupertino.CupertinoSwitch.dragStartBehavior} /// Determines the way that drag start behavior is handled. /// @@ -310,6 +324,7 @@ class _CupertinoSwitchState extends State with TickerProviderSt @override Widget build(BuildContext context) { + final CupertinoThemeData theme = CupertinoTheme.of(context); if (needsPositionAnimation) { _resumePositionAnimation(); } @@ -320,7 +335,9 @@ class _CupertinoSwitchState extends State with TickerProviderSt child: _CupertinoSwitchRenderObjectWidget( value: widget.value, activeColor: CupertinoDynamicColor.resolve( - widget.activeColor ?? CupertinoColors.systemGreen, + widget.activeColor + ?? ((widget.applyTheme ?? theme.applyThemeToAll) ? theme.primaryColor : null) + ?? CupertinoColors.systemGreen, context, ), trackColor: CupertinoDynamicColor.resolve(widget.trackColor ?? CupertinoColors.secondarySystemFill, context), diff --git a/packages/flutter/lib/src/cupertino/theme.dart b/packages/flutter/lib/src/cupertino/theme.dart index f68c3f417f33..3d8d493cc038 100644 --- a/packages/flutter/lib/src/cupertino/theme.dart +++ b/packages/flutter/lib/src/cupertino/theme.dart @@ -22,6 +22,7 @@ const _CupertinoThemeDefaults _kDefaultTheme = _CupertinoThemeDefaults( // Values extracted from navigation bar. For toolbar or tabbar the dark color is 0xF0161616. ), CupertinoColors.systemBackground, + false, _CupertinoTextThemeDefaults(CupertinoColors.label, CupertinoColors.inactiveGray), ); @@ -172,6 +173,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable CupertinoTextThemeData? textTheme, Color? barBackgroundColor, Color? scaffoldBackgroundColor, + bool? applyThemeToAll, }) : this.raw( brightness, primaryColor, @@ -179,6 +181,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable textTheme, barBackgroundColor, scaffoldBackgroundColor, + applyThemeToAll, ); /// Same as the default constructor but with positional arguments to avoid @@ -193,6 +196,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable CupertinoTextThemeData? textTheme, Color? barBackgroundColor, Color? scaffoldBackgroundColor, + bool? applyThemeToAll, ) : this._rawWithDefaults( brightness, primaryColor, @@ -200,6 +204,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable textTheme, barBackgroundColor, scaffoldBackgroundColor, + applyThemeToAll, _kDefaultTheme, ); @@ -210,6 +215,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable CupertinoTextThemeData? textTheme, Color? barBackgroundColor, Color? scaffoldBackgroundColor, + bool? applyThemeToAll, this._defaults, ) : super( brightness: brightness, @@ -218,6 +224,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable textTheme: textTheme, barBackgroundColor: barBackgroundColor, scaffoldBackgroundColor: scaffoldBackgroundColor, + applyThemeToAll: applyThemeToAll, ); final _CupertinoThemeDefaults _defaults; @@ -239,6 +246,9 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable @override Color get scaffoldBackgroundColor => super.scaffoldBackgroundColor ?? _defaults.scaffoldBackgroundColor; + @override + bool get applyThemeToAll => super.applyThemeToAll ?? _defaults.applyThemeToAll; + @override NoDefaultCupertinoThemeData noDefault() { return NoDefaultCupertinoThemeData( @@ -248,6 +258,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable textTheme: super.textTheme, barBackgroundColor: super.barBackgroundColor, scaffoldBackgroundColor: super.scaffoldBackgroundColor, + applyThemeToAll: super.applyThemeToAll, ); } @@ -262,6 +273,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable super.textTheme?.resolveFrom(context), convertColor(super.barBackgroundColor), convertColor(super.scaffoldBackgroundColor), + applyThemeToAll, _defaults.resolveFrom(context, super.textTheme == null), ); } @@ -274,6 +286,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable CupertinoTextThemeData? textTheme, Color? barBackgroundColor, Color? scaffoldBackgroundColor, + bool? applyThemeToAll, }) { return CupertinoThemeData._rawWithDefaults( brightness ?? super.brightness, @@ -282,6 +295,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable textTheme ?? super.textTheme, barBackgroundColor ?? super.barBackgroundColor, scaffoldBackgroundColor ?? super.scaffoldBackgroundColor, + applyThemeToAll ?? super.applyThemeToAll, _defaults, ); } @@ -295,6 +309,7 @@ class CupertinoThemeData extends NoDefaultCupertinoThemeData with Diagnosticable properties.add(createCupertinoColorProperty('primaryContrastingColor', primaryContrastingColor, defaultValue: defaultData.primaryContrastingColor)); properties.add(createCupertinoColorProperty('barBackgroundColor', barBackgroundColor, defaultValue: defaultData.barBackgroundColor)); properties.add(createCupertinoColorProperty('scaffoldBackgroundColor', scaffoldBackgroundColor, defaultValue: defaultData.scaffoldBackgroundColor)); + properties.add(DiagnosticsProperty('applyThemeToAll', applyThemeToAll, defaultValue: defaultData.applyThemeToAll)); textTheme.debugFillProperties(properties); } } @@ -322,6 +337,7 @@ class NoDefaultCupertinoThemeData { this.textTheme, this.barBackgroundColor, this.scaffoldBackgroundColor, + this.applyThemeToAll, }); /// The brightness override for Cupertino descendants. @@ -389,6 +405,22 @@ class NoDefaultCupertinoThemeData { /// Defaults to [CupertinoColors.systemBackground]. final Color? scaffoldBackgroundColor; + /// Flag to apply this theme to all descendant Cupertino widgets. + /// + /// Certain Cupertino widgets previously didn't use theming, matching past + /// versions of iOS. For example, [CupertinoSwitch]s always used + /// [CupertinoColors.systemGreen] when active. + /// + /// Today, however, these widgets can indeed be themed on iOS. Moreover on + /// macOS, the accent color is reflected in these widgets. Turning this flag + /// on ensures that descendant Cupertino widgets will be themed accordingly. + /// + /// This flag currently applies to the following widgets: + /// - [CupertinoSwitch] & [Switch.adaptive] + /// + /// Defaults to false. + final bool? applyThemeToAll; + /// Returns an instance of the theme data whose property getters only return /// the construction time specifications with no derived values. /// @@ -412,6 +444,7 @@ class NoDefaultCupertinoThemeData { textTheme: textTheme?.resolveFrom(context), barBackgroundColor: convertColor(barBackgroundColor), scaffoldBackgroundColor: convertColor(scaffoldBackgroundColor), + applyThemeToAll: applyThemeToAll, ); } @@ -428,6 +461,7 @@ class NoDefaultCupertinoThemeData { CupertinoTextThemeData? textTheme, Color? barBackgroundColor , Color? scaffoldBackgroundColor, + bool? applyThemeToAll, }) { return NoDefaultCupertinoThemeData( brightness: brightness ?? this.brightness, @@ -436,6 +470,7 @@ class NoDefaultCupertinoThemeData { textTheme: textTheme ?? this.textTheme, barBackgroundColor: barBackgroundColor ?? this.barBackgroundColor, scaffoldBackgroundColor: scaffoldBackgroundColor ?? this.scaffoldBackgroundColor, + applyThemeToAll: applyThemeToAll ?? this.applyThemeToAll, ); } } @@ -448,6 +483,7 @@ class _CupertinoThemeDefaults { this.primaryContrastingColor, this.barBackgroundColor, this.scaffoldBackgroundColor, + this.applyThemeToAll, this.textThemeDefaults, ); @@ -456,6 +492,7 @@ class _CupertinoThemeDefaults { final Color primaryContrastingColor; final Color barBackgroundColor; final Color scaffoldBackgroundColor; + final bool applyThemeToAll; final _CupertinoTextThemeDefaults textThemeDefaults; _CupertinoThemeDefaults resolveFrom(BuildContext context, bool resolveTextTheme) { @@ -467,6 +504,7 @@ class _CupertinoThemeDefaults { convertColor(primaryContrastingColor), convertColor(barBackgroundColor), convertColor(scaffoldBackgroundColor), + applyThemeToAll, resolveTextTheme ? textThemeDefaults.resolveFrom(context) : textThemeDefaults, ); } diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index a60565258f8b..a2047a86cd86 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -117,6 +117,7 @@ class Switch extends StatelessWidget { this.onFocusChange, this.autofocus = false, }) : _switchType = _SwitchType.material, + applyCupertinoTheme = false, assert(dragStartBehavior != null), assert(activeThumbImage != null || onActiveThumbImageError == null), assert(inactiveThumbImage != null || onInactiveThumbImageError == null); @@ -161,6 +162,7 @@ class Switch extends StatelessWidget { this.focusNode, this.onFocusChange, this.autofocus = false, + this.applyCupertinoTheme, }) : assert(autofocus != null), assert(activeThumbImage != null || onActiveThumbImageError == null), assert(inactiveThumbImage != null || onInactiveThumbImageError == null), @@ -381,6 +383,9 @@ class Switch extends StatelessWidget { final _SwitchType _switchType; + /// {@macro flutter.cupertino.CupertinoSwitch.applyTheme} + final bool? applyCupertinoTheme; + /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior} final DragStartBehavior dragStartBehavior; @@ -495,6 +500,7 @@ class Switch extends StatelessWidget { onChanged: onChanged, activeColor: activeColor, trackColor: inactiveTrackColor, + applyTheme: applyCupertinoTheme, ), ), ); diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 29a234764b99..08dd5a237d41 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -2703,6 +2703,7 @@ class MaterialBasedCupertinoThemeData extends CupertinoThemeData { _cupertinoOverrideTheme.textTheme, _cupertinoOverrideTheme.barBackgroundColor, _cupertinoOverrideTheme.scaffoldBackgroundColor, + _cupertinoOverrideTheme.applyThemeToAll, ); final ThemeData _materialTheme; @@ -2738,6 +2739,7 @@ class MaterialBasedCupertinoThemeData extends CupertinoThemeData { CupertinoTextThemeData? textTheme, Color? barBackgroundColor, Color? scaffoldBackgroundColor, + bool? applyThemeToAll, }) { return MaterialBasedCupertinoThemeData._( _materialTheme, @@ -2748,6 +2750,7 @@ class MaterialBasedCupertinoThemeData extends CupertinoThemeData { textTheme: textTheme, barBackgroundColor: barBackgroundColor, scaffoldBackgroundColor: scaffoldBackgroundColor, + applyThemeToAll: applyThemeToAll, ), ); } diff --git a/packages/flutter/test/cupertino/switch_test.dart b/packages/flutter/test/cupertino/switch_test.dart index 5cd61348f611..025275a7fbc7 100644 --- a/packages/flutter/test/cupertino/switch_test.dart +++ b/packages/flutter/test/cupertino/switch_test.dart @@ -764,6 +764,66 @@ void main() { ); }); + testWidgets('Switch can apply the ambient theme and be opted out', (WidgetTester tester) async { + final Key switchKey = UniqueKey(); + bool value = false; + await tester.pumpWidget( + CupertinoTheme( + data: const CupertinoThemeData(primaryColor: Colors.amber, applyThemeToAll: true), + child: Directionality( + textDirection: TextDirection.ltr, + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Center( + child: RepaintBoundary( + child: Column( + children: [ + CupertinoSwitch( + key: switchKey, + value: value, + dragStartBehavior: DragStartBehavior.down, + applyTheme: true, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + }, + ), + CupertinoSwitch( + value: value, + dragStartBehavior: DragStartBehavior.down, + applyTheme: false, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ); + + await expectLater( + find.byType(Column), + matchesGoldenFile('switch.tap.off.themed.png'), + ); + + await tester.tap(find.byKey(switchKey)); + expect(value, isTrue); + + await tester.pumpAndSettle(); + await expectLater( + find.byType(Column), + matchesGoldenFile('switch.tap.on.themed.png'), + ); + }); + testWidgets('Hovering over Cupertino switch updates cursor to clickable on Web', (WidgetTester tester) async { const bool value = false; // Disabled CupertinoSwitch does not update cursor on Web. diff --git a/packages/flutter/test/cupertino/theme_test.dart b/packages/flutter/test/cupertino/theme_test.dart index 87b8665abc4a..84058d75ff16 100644 --- a/packages/flutter/test/cupertino/theme_test.dart +++ b/packages/flutter/test/cupertino/theme_test.dart @@ -52,6 +52,7 @@ void main() { expect(theme.brightness, isNull); expect(theme.primaryColor, CupertinoColors.activeBlue); expect(theme.textTheme.textStyle.fontSize, 17.0); + expect(theme.applyThemeToAll, false); }); testWidgets('Theme attributes cascade', (WidgetTester tester) async { @@ -122,10 +123,12 @@ void main() { (WidgetTester tester) async { const CupertinoThemeData originalTheme = CupertinoThemeData( brightness: Brightness.dark, + applyThemeToAll: true, ); final CupertinoThemeData theme = await testTheme(tester, originalTheme.copyWith( primaryColor: CupertinoColors.systemGreen, + applyThemeToAll: false, )); expect(theme.brightness, Brightness.dark); @@ -133,6 +136,8 @@ void main() { // Now check calculated derivatives. expect(theme.textTheme.actionTextStyle.color, isSameColorAs(CupertinoColors.systemGreen.darkColor)); expect(theme.scaffoldBackgroundColor, isSameColorAs(CupertinoColors.black)); + + expect(theme.applyThemeToAll, false); }, ); @@ -181,6 +186,7 @@ void main() { 'primaryContrastingColor', 'barBackgroundColor', 'scaffoldBackgroundColor', + 'applyThemeToAll', 'textStyle', 'actionTextStyle', 'tabLabelTextStyle',