From bbb080b3a3d4e2f3b61b2f7b029cafdb9b9cab99 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 8 Oct 2018 10:39:59 -0700 Subject: [PATCH] Material Switch optionally adapts per platform: Switch.adaptive() (#22688) --- .../material/selection_controls_demo.dart | 6 +- packages/flutter/lib/src/material/switch.dart | 112 +++++++++++++++--- .../flutter/test/material/switch_test.dart | 44 +++++++ 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/material/selection_controls_demo.dart b/examples/flutter_gallery/lib/demo/material/selection_controls_demo.dart index 6f8be196e0..c115ec06dd 100644 --- a/examples/flutter_gallery/lib/demo/material/selection_controls_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/selection_controls_demo.dart @@ -187,7 +187,7 @@ class _SelectionControlsDemoState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Switch( + Switch.adaptive( value: switchValue, onChanged: (bool value) { setState(() { @@ -196,8 +196,8 @@ class _SelectionControlsDemoState extends State { } ), // Disabled switches - const Switch(value: true, onChanged: null), - const Switch(value: false, onChanged: null) + const Switch.adaptive(value: true, onChanged: null), + const Switch.adaptive(value: false, onChanged: null), ], ), ); diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index e3ee893019..4f95ce258a 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; @@ -23,6 +24,8 @@ const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReact const double _kSwitchHeight = 2 * kRadialReactionRadius + 8.0; const double _kSwitchHeightCollapsed = 2 * kRadialReactionRadius; +enum _SwitchType { material, adaptive } + /// A material design switch. /// /// Used to toggle the on/off state of a single setting. @@ -65,7 +68,30 @@ class Switch extends StatefulWidget { this.activeThumbImage, this.inactiveThumbImage, this.materialTapTargetSize, - }) : super(key: key); + }) : _switchType = _SwitchType.material, + super(key: key); + + /// Creates a [CupertinoSwitch] if the target platform is iOS, creates a + /// material design switch otherwise. + /// + /// If a [CupertinoSwitch] is created, the following parameters are + /// ignored: [activeTrackColor], [inactiveThumbColor], [inactiveTrackColor], + /// [activeThumbImage], [inactiveThumbImage], [materialTapTargetSize]. + /// + /// The target platform is based on the current [Theme]: [ThemeData.platform]. + const Switch.adaptive({ + Key key, + @required this.value, + @required this.onChanged, + this.activeColor, + this.activeTrackColor, + this.inactiveThumbColor, + this.inactiveTrackColor, + this.activeThumbImage, + this.inactiveThumbImage, + this.materialTapTargetSize, + }) : _switchType = _SwitchType.adaptive, + super(key: key); /// Whether this switch is on or off. /// @@ -104,22 +130,32 @@ class Switch extends StatefulWidget { /// The color to use on the track when this switch is on. /// /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%. + /// + /// Ignored if this switch is created with [Switch.adaptive]. final Color activeTrackColor; /// The color to use on the thumb when this switch is off. /// /// Defaults to the colors described in the Material design specification. + /// + /// Ignored if this switch is created with [Switch.adaptive]. final Color inactiveThumbColor; /// The color to use on the track when this switch is off. /// /// Defaults to the colors described in the Material design specification. + /// + /// Ignored if this switch is created with [Switch.adaptive]. final Color inactiveTrackColor; /// An image to use on the thumb of this switch when the switch is on. + /// + /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider activeThumbImage; /// An image to use on the thumb of this switch when the switch is off. + /// + /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider inactiveThumbImage; /// Configures the minimum size of the tap target. @@ -131,6 +167,8 @@ class Switch extends StatefulWidget { /// * [MaterialTapTargetSize], for a description of how this affects tap targets. final MaterialTapTargetSize materialTapTargetSize; + final _SwitchType _switchType; + @override _SwitchState createState() => _SwitchState(); @@ -143,13 +181,25 @@ class Switch extends StatefulWidget { } class _SwitchState extends State with TickerProviderStateMixin { - @override - Widget build(BuildContext context) { - assert(debugCheckHasMaterial(context)); - final ThemeData themeData = Theme.of(context); - final bool isDark = themeData.brightness == Brightness.dark; + Size getSwitchSize(ThemeData theme) { + switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) { + case MaterialTapTargetSize.padded: + return const Size(_kSwitchWidth, _kSwitchHeight); + break; + case MaterialTapTargetSize.shrinkWrap: + return const Size(_kSwitchWidth, _kSwitchHeightCollapsed); + break; + } + assert(false); + return null; + } - final Color activeThumbColor = widget.activeColor ?? themeData.toggleableActiveColor; + Widget buildMaterialSwitch(BuildContext context) { + assert(debugCheckHasMaterial(context)); + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; + + final Color activeThumbColor = widget.activeColor ?? theme.toggleableActiveColor; final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80); Color inactiveThumbColor; @@ -162,16 +212,6 @@ class _SwitchState extends State with TickerProviderStateMixin { inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400); inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12); } - Size size; - switch (widget.materialTapTargetSize ?? themeData.materialTapTargetSize) { - case MaterialTapTargetSize.padded: - size = const Size(_kSwitchWidth, _kSwitchHeight); - break; - case MaterialTapTargetSize.shrinkWrap: - size = const Size(_kSwitchWidth, _kSwitchHeightCollapsed); - break; - } - final BoxConstraints additionalConstraints = BoxConstraints.tight(size); return _SwitchRenderObjectWidget( value: widget.value, @@ -183,10 +223,46 @@ class _SwitchState extends State with TickerProviderStateMixin { inactiveTrackColor: inactiveTrackColor, configuration: createLocalImageConfiguration(context), onChanged: widget.onChanged, - additionalConstraints: additionalConstraints, + additionalConstraints: BoxConstraints.tight(getSwitchSize(theme)), vsync: this, ); } + + Widget buildCupertinoSwitch(BuildContext context) { + final Size size = getSwitchSize(Theme.of(context)); + return Container( + width: size.width, // Same size as the Material switch. + height: size.height, + alignment: Alignment.center, + child: CupertinoSwitch( + value: widget.value, + onChanged: widget.onChanged, + activeColor: widget.activeColor, + ), + ); + } + + @override + Widget build(BuildContext context) { + switch (widget._switchType) { + case _SwitchType.material: + return buildMaterialSwitch(context); + + case _SwitchType.adaptive: { + final ThemeData theme = Theme.of(context); + assert(theme.platform != null); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + return buildMaterialSwitch(context); + case TargetPlatform.iOS: + return buildCupertinoSwitch(context); + } + } + } + assert(false); + return null; + } } class _SwitchRenderObjectWidget extends LeafRenderObjectWidget { diff --git a/packages/flutter/test/material/switch_test.dart b/packages/flutter/test/material/switch_test.dart index cf89d2aa44..59a7c5f827 100644 --- a/packages/flutter/test/material/switch_test.dart +++ b/packages/flutter/test/material/switch_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -485,4 +487,46 @@ void main() { semanticsTester.dispose(); SystemChannels.accessibility.setMockMessageHandler(null); }); + + testWidgets('Switch.adaptive', (WidgetTester tester) async { + bool value = false; + + Widget buildFrame(TargetPlatform platform) { + return MaterialApp( + theme: ThemeData(platform: platform), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Material( + child: Center( + child: Switch.adaptive( + value: value, + onChanged: (bool newValue) { + setState(() { + value = newValue; + }); + }, + ), + ), + ); + }, + ), + ); + } + + await tester.pumpWidget(buildFrame(TargetPlatform.iOS)); + expect(find.byType(CupertinoSwitch), findsOneWidget); + + expect(value, isFalse); + await tester.tap(find.byType(Switch)); + expect(value, isTrue); + + await tester.pumpWidget(buildFrame(TargetPlatform.android)); + await tester.pumpAndSettle(); // Finish the theme change animation. + expect(find.byType(CupertinoSwitch), findsNothing); + expect(value, isTrue); + await tester.tap(find.byType(Switch)); + expect(value, isFalse); + + }); + }