From c8a3dbaf0621da7b665e4218e66f13ceb8c67e1f Mon Sep 17 00:00:00 2001 From: Mitchell Goodwin <58190796+MitchellGoodwin@users.noreply.github.com> Date: Tue, 4 Apr 2023 13:33:02 -0700 Subject: [PATCH] Add adaptive constructor to Radio and RadioListTile (#123816) Add adaptive constructor to Radio and RadioListTile --- packages/flutter/lib/src/material/radio.dart | 68 +++++++++++++- .../lib/src/material/radio_list_tile.dart | 93 ++++++++++++++++--- .../test/material/radio_list_tile_test.dart | 32 +++++++ .../flutter/test/material/radio_test.dart | 32 +++++++ 4 files changed, 208 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 05fbbb6f31..7a7622edf3 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -2,7 +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/widgets.dart'; +import 'package:flutter/cupertino.dart'; import 'color_scheme.dart'; import 'colors.dart'; @@ -20,6 +20,8 @@ import 'toggleable.dart'; // late SingingCharacter? _character; // late StateSetter setState; +enum _RadioType { material, adaptive } + const double _kOuterRadius = 8.0; const double _kInnerRadius = 4.5; @@ -93,7 +95,40 @@ class Radio extends StatefulWidget { this.visualDensity, this.focusNode, this.autofocus = false, - }); + }) : _radioType = _RadioType.material; + + /// Creates an adaptive [Radio] based on whether the target platform is iOS + /// 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 [CupertinoRadio], which has + /// matching functionality and presentation as Material checkboxes, and are the + /// graphics expected on iOS. On other platforms, this creates a Material + /// design [Radio]. + /// + /// If a [CupertinoRadio] is created, the following parameters are ignored: + /// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius], + /// [materialTapTargetSize], [visualDensity]. + /// + /// The target platform is based on the current [Theme]: [ThemeData.platform]. + const Radio.adaptive({ + super.key, + required this.value, + required this.groupValue, + required this.onChanged, + this.mouseCursor, + this.toggleable = false, + this.activeColor, + this.fillColor, + this.focusColor, + this.hoverColor, + this.overlayColor, + this.splashRadius, + this.materialTapTargetSize, + this.visualDensity, + this.focusNode, + this.autofocus = false, + }) : _radioType = _RadioType.adaptive; /// The value represented by this radio button. final T value; @@ -309,6 +344,8 @@ class Radio extends StatefulWidget { /// {@macro flutter.widgets.Focus.autofocus} final bool autofocus; + final _RadioType _radioType; + bool get _selected => value == groupValue; @override @@ -366,6 +403,33 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); + switch (widget._radioType) { + case _RadioType.material: + break; + + case _RadioType.adaptive: + final ThemeData theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + break; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return CupertinoRadio( + value: widget.value, + groupValue: widget.groupValue, + onChanged: widget.onChanged, + toggleable: widget.toggleable, + activeColor: widget.activeColor, + focusColor: widget.focusColor, + focusNode: widget.focusNode, + autofocus: widget.autofocus, + ); + } + } + final RadioThemeData radioTheme = RadioTheme.of(context); final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context); final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 292748ab2f..62a80c46d1 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -18,6 +18,8 @@ import 'theme_data.dart'; // enum SingingCharacter { lafayette } // late SingingCharacter? _character; +enum _RadioType { material, adaptive } + /// A [ListTile] with a [Radio]. In other words, a radio button with a label. /// /// The entire list tile is interactive: tapping anywhere in the tile selects @@ -186,7 +188,46 @@ class RadioListTile extends StatelessWidget { this.focusNode, this.onFocusChange, this.enableFeedback, - }) : assert(!isThreeLine || subtitle != null); + }) : _radioType = _RadioType.material, + assert(!isThreeLine || subtitle != null); + + /// Creates a combination of a list tile and a platform adaptive radio. + /// + /// The checkbox uses [Radio.adaptive] to show a [CupertinoRadio] for + /// iOS platforms, or [Radio] for all others. + /// + /// All other properties are the same as [RadioListTile]. + const RadioListTile.adaptive({ + super.key, + required this.value, + required this.groupValue, + required this.onChanged, + this.mouseCursor, + this.toggleable = false, + this.activeColor, + this.fillColor, + this.hoverColor, + this.overlayColor, + this.splashRadius, + this.materialTapTargetSize, + this.title, + this.subtitle, + this.isThreeLine = false, + this.dense, + this.secondary, + this.selected = false, + this.controlAffinity = ListTileControlAffinity.platform, + this.autofocus = false, + this.contentPadding, + this.shape, + this.tileColor, + this.selectedTileColor, + this.visualDensity, + this.focusNode, + this.onFocusChange, + this.enableFeedback, + }) : _radioType = _RadioType.adaptive, + assert(!isThreeLine || subtitle != null); /// The value represented by this radio button. final T value; @@ -392,22 +433,44 @@ class RadioListTile extends StatelessWidget { /// * [Feedback] for providing platform-specific feedback to certain actions. final bool? enableFeedback; + final _RadioType _radioType; + @override Widget build(BuildContext context) { - final Widget control = Radio( - value: value, - groupValue: groupValue, - onChanged: onChanged, - toggleable: toggleable, - activeColor: activeColor, - materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, - autofocus: autofocus, - fillColor: fillColor, - mouseCursor: mouseCursor, - hoverColor: hoverColor, - overlayColor: overlayColor, - splashRadius: splashRadius, - ); + final Widget control; + switch (_radioType) { + case _RadioType.material: + control = Radio( + value: value, + groupValue: groupValue, + onChanged: onChanged, + toggleable: toggleable, + activeColor: activeColor, + materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, + autofocus: autofocus, + fillColor: fillColor, + mouseCursor: mouseCursor, + hoverColor: hoverColor, + overlayColor: overlayColor, + splashRadius: splashRadius, + ); + case _RadioType.adaptive: + control = Radio.adaptive( + value: value, + groupValue: groupValue, + onChanged: onChanged, + toggleable: toggleable, + activeColor: activeColor, + materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, + autofocus: autofocus, + fillColor: fillColor, + mouseCursor: mouseCursor, + hoverColor: hoverColor, + overlayColor: overlayColor, + splashRadius: splashRadius, + ); + } + Widget? leading, trailing; switch (controlAffinity) { case ListTileControlAffinity.leading: diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart index f9470626ef..c852c9fd78 100644 --- a/packages/flutter/test/material/radio_list_tile_test.dart +++ b/packages/flutter/test/material/radio_list_tile_test.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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1241,6 +1242,37 @@ void main() { expect(tester.getSize(find.byType(Radio)), const Size(48.0, 48.0)); }); + testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async { + Widget buildApp(TargetPlatform platform) { + return MaterialApp( + theme: ThemeData(platform: platform), + home: Material( + child: Center( + child: RadioListTile.adaptive( + value: 1, + groupValue: 2, + onChanged: (_) {}, + ), + ), + ), + ); + } + + for (final TargetPlatform platform in [ TargetPlatform.iOS, TargetPlatform.macOS ]) { + await tester.pumpWidget(buildApp(platform)); + await tester.pumpAndSettle(); + + expect(find.byType(CupertinoRadio), findsOneWidget); + } + + for (final TargetPlatform platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) { + await tester.pumpWidget(buildApp(platform)); + await tester.pumpAndSettle(); + + expect(find.byType(CupertinoRadio), findsNothing); + } + }); + group('feedback', () { late FeedbackTester feedback; diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index 6e93da5674..f10efde59a 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -9,6 +9,7 @@ library; import 'dart:ui'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -1372,4 +1373,35 @@ void main() { : (paints..circle(color: theme.hoverColor)..circle(color: colors.secondary)) ); }); + + testWidgets('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async { + Widget buildApp(TargetPlatform platform) { + return MaterialApp( + theme: ThemeData(platform: platform), + home: Material( + child: Center( + child: Radio.adaptive( + value: 1, + groupValue: 2, + onChanged: (_) {}, + ), + ), + ), + ); + } + + for (final TargetPlatform platform in [ TargetPlatform.iOS, TargetPlatform.macOS ]) { + await tester.pumpWidget(buildApp(platform)); + await tester.pumpAndSettle(); + + expect(find.byType(CupertinoRadio), findsOneWidget); + } + + for (final TargetPlatform platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) { + await tester.pumpWidget(buildApp(platform)); + await tester.pumpAndSettle(); + + expect(find.byType(CupertinoRadio), findsNothing); + } + }); }