Unverified Commit c8a3dbaf authored by Mitchell Goodwin's avatar Mitchell Goodwin Committed by GitHub

Add adaptive constructor to Radio and RadioListTile (#123816)

Add adaptive constructor to Radio and RadioListTile
parent d6593dee
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/widgets.dart'; import 'package:flutter/cupertino.dart';
import 'color_scheme.dart'; import 'color_scheme.dart';
import 'colors.dart'; import 'colors.dart';
...@@ -20,6 +20,8 @@ import 'toggleable.dart'; ...@@ -20,6 +20,8 @@ import 'toggleable.dart';
// late SingingCharacter? _character; // late SingingCharacter? _character;
// late StateSetter setState; // late StateSetter setState;
enum _RadioType { material, adaptive }
const double _kOuterRadius = 8.0; const double _kOuterRadius = 8.0;
const double _kInnerRadius = 4.5; const double _kInnerRadius = 4.5;
...@@ -93,7 +95,40 @@ class Radio<T> extends StatefulWidget { ...@@ -93,7 +95,40 @@ class Radio<T> extends StatefulWidget {
this.visualDensity, this.visualDensity,
this.focusNode, this.focusNode,
this.autofocus = false, 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. /// The value represented by this radio button.
final T value; final T value;
...@@ -309,6 +344,8 @@ class Radio<T> extends StatefulWidget { ...@@ -309,6 +344,8 @@ class Radio<T> extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus} /// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus; final bool autofocus;
final _RadioType _radioType;
bool get _selected => value == groupValue; bool get _selected => value == groupValue;
@override @override
...@@ -366,6 +403,33 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg ...@@ -366,6 +403,33 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(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<T>(
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 radioTheme = RadioTheme.of(context);
final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context); final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context);
final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize
......
...@@ -18,6 +18,8 @@ import 'theme_data.dart'; ...@@ -18,6 +18,8 @@ import 'theme_data.dart';
// enum SingingCharacter { lafayette } // enum SingingCharacter { lafayette }
// late SingingCharacter? _character; // late SingingCharacter? _character;
enum _RadioType { material, adaptive }
/// A [ListTile] with a [Radio]. In other words, a radio button with a label. /// 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 /// The entire list tile is interactive: tapping anywhere in the tile selects
...@@ -186,7 +188,46 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -186,7 +188,46 @@ class RadioListTile<T> extends StatelessWidget {
this.focusNode, this.focusNode,
this.onFocusChange, this.onFocusChange,
this.enableFeedback, 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. /// The value represented by this radio button.
final T value; final T value;
...@@ -392,22 +433,44 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -392,22 +433,44 @@ class RadioListTile<T> extends StatelessWidget {
/// * [Feedback] for providing platform-specific feedback to certain actions. /// * [Feedback] for providing platform-specific feedback to certain actions.
final bool? enableFeedback; final bool? enableFeedback;
final _RadioType _radioType;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget control = Radio<T>( final Widget control;
value: value, switch (_radioType) {
groupValue: groupValue, case _RadioType.material:
onChanged: onChanged, control = Radio<T>(
toggleable: toggleable, value: value,
activeColor: activeColor, groupValue: groupValue,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, onChanged: onChanged,
autofocus: autofocus, toggleable: toggleable,
fillColor: fillColor, activeColor: activeColor,
mouseCursor: mouseCursor, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
hoverColor: hoverColor, autofocus: autofocus,
overlayColor: overlayColor, fillColor: fillColor,
splashRadius: splashRadius, mouseCursor: mouseCursor,
); hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
);
case _RadioType.adaptive:
control = Radio<T>.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; Widget? leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
case ListTileControlAffinity.leading: case ListTileControlAffinity.leading:
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -1241,6 +1242,37 @@ void main() { ...@@ -1241,6 +1242,37 @@ void main() {
expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0)); expect(tester.getSize(find.byType(Radio<bool>)), 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<int>.adaptive(
value: 1,
groupValue: 2,
onChanged: (_) {},
),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoRadio<int>), findsOneWidget);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoRadio<int>), findsNothing);
}
});
group('feedback', () { group('feedback', () {
late FeedbackTester feedback; late FeedbackTester feedback;
......
...@@ -9,6 +9,7 @@ library; ...@@ -9,6 +9,7 @@ library;
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -1372,4 +1373,35 @@ void main() { ...@@ -1372,4 +1373,35 @@ void main() {
: (paints..circle(color: theme.hoverColor)..circle(color: colors.secondary)) : (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<int>.adaptive(
value: 1,
groupValue: 2,
onChanged: (_) {},
),
),
),
);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoRadio<int>), findsOneWidget);
}
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
await tester.pumpWidget(buildApp(platform));
await tester.pumpAndSettle();
expect(find.byType(CupertinoRadio<int>), findsNothing);
}
});
} }
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