diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 1806d79177..21157d72cb 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -31,6 +31,7 @@ import 'package:gen_defaults/input_chip_template.dart'; import 'package:gen_defaults/input_decorator_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart'; +import 'package:gen_defaults/radio_template.dart'; import 'package:gen_defaults/surface_tint.dart'; import 'package:gen_defaults/switch_template.dart'; import 'package:gen_defaults/text_field_template.dart'; @@ -79,6 +80,7 @@ Future main(List args) async { 'navigation_drawer.json', 'navigation_rail.json', 'palette.json', + 'radio_button.json', 'segmented_button_outlined.json', 'shape.json', 'slider.json', @@ -124,6 +126,7 @@ Future main(List args) async { InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile(); + RadioTemplate('Radio', '$materialLib/radio.dart', tokens).updateFile(); SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile(); SwitchTemplate('Switch', '$materialLib/switch.dart', tokens).updateFile(); TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/radio_template.dart b/dev/tools/gen_defaults/lib/radio_template.dart new file mode 100644 index 0000000000..0d4ee453eb --- /dev/null +++ b/dev/tools/gen_defaults/lib/radio_template.dart @@ -0,0 +1,90 @@ +// 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 'template.dart'; + +class RadioTemplate extends TokenTemplate { + const RadioTemplate(super.blockName, super.fileName, super.tokens, { + super.colorSchemePrefix = '_colors.', + }); + + @override + String generate() => ''' +class _RadioDefaultsM3 extends RadioThemeData { + _RadioDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + MaterialStateProperty get fillColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.disabled)) { + return ${componentColor('md.comp.radio-button.disabled.selected.icon')}; + } + if (states.contains(MaterialState.pressed)) { + return ${componentColor('md.comp.radio-button.selected.pressed.icon')}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor('md.comp.radio-button.selected.hover.icon')}; + } + if (states.contains(MaterialState.focused)) { + return ${componentColor('md.comp.radio-button.selected.focus.icon')}; + } + return ${componentColor('md.comp.radio-button.selected.icon')}; + } + if (states.contains(MaterialState.disabled)) { + return ${componentColor('md.comp.radio-button.disabled.unselected.icon')}; + } + if (states.contains(MaterialState.pressed)) { + return ${componentColor('md.comp.radio-button.unselected.pressed.icon')}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor('md.comp.radio-button.unselected.hover.icon')}; + } + if (states.contains(MaterialState.focused)) { + return ${componentColor('md.comp.radio-button.unselected.focus.icon')}; + } + return ${componentColor('md.comp.radio-button.unselected.icon')}; + }); + } + + @override + MaterialStateProperty get overlayColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.pressed)) { + return ${componentColor('md.comp.radio-button.selected.pressed.state-layer')}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor('md.comp.radio-button.selected.hover.state-layer')}; + } + if (states.contains(MaterialState.focused)) { + return ${componentColor('md.comp.radio-button.selected.focus.state-layer')}; + } + return Colors.transparent; + } + if (states.contains(MaterialState.pressed)) { + return ${componentColor('md.comp.radio-button.unselected.pressed.state-layer')}; + } + if (states.contains(MaterialState.hovered)) { + return ${componentColor('md.comp.radio-button.unselected.hover.state-layer')}; + } + if (states.contains(MaterialState.focused)) { + return ${componentColor('md.comp.radio-button.unselected.focus.state-layer')}; + } + return Colors.transparent; + }); + } + + @override + MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize; + + @override + VisualDensity get visualDensity => _theme.visualDensity; +} +'''; +} diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 6f047cd498..6041777b04 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -4,6 +4,8 @@ import 'package:flutter/widgets.dart'; +import 'color_scheme.dart'; +import 'colors.dart'; import 'constants.dart'; import 'debug.dart'; import 'material_state.dart'; @@ -360,30 +362,17 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg }); } - MaterialStateProperty get _defaultFillColor { - final ThemeData themeData = Theme.of(context); - return MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return themeData.disabledColor; - } - if (states.contains(MaterialState.selected)) { - return themeData.colorScheme.secondary; - } - return themeData.unselectedWidgetColor; - }); - } - @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); - final ThemeData themeData = Theme.of(context); final RadioThemeData radioTheme = RadioTheme.of(context); + final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context); final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize ?? radioTheme.materialTapTargetSize - ?? themeData.materialTapTargetSize; + ?? defaults.materialTapTargetSize!; final VisualDensity effectiveVisualDensity = widget.visualDensity ?? radioTheme.visualDensity - ?? themeData.visualDensity; + ?? defaults.visualDensity!; Size size; switch (effectiveMaterialTapTargetSize) { case MaterialTapTargetSize.padded: @@ -405,36 +394,47 @@ class _RadioState extends State> with TickerProviderStateMixin, Togg // so that they can be lerped between. final Set activeStates = states..add(MaterialState.selected); final Set inactiveStates = states..remove(MaterialState.selected); - final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates) + final Color? activeColor = widget.fillColor?.resolve(activeStates) ?? _widgetFillColor.resolve(activeStates) - ?? radioTheme.fillColor?.resolve(activeStates) - ?? _defaultFillColor.resolve(activeStates); - final Color effectiveInactiveColor = widget.fillColor?.resolve(inactiveStates) + ?? radioTheme.fillColor?.resolve(activeStates); + final Color effectiveActiveColor = activeColor ?? defaults.fillColor!.resolve(activeStates)!; + final Color? inactiveColor = widget.fillColor?.resolve(inactiveStates) ?? _widgetFillColor.resolve(inactiveStates) - ?? radioTheme.fillColor?.resolve(inactiveStates) - ?? _defaultFillColor.resolve(inactiveStates); + ?? radioTheme.fillColor?.resolve(inactiveStates); + final Color effectiveInactiveColor = inactiveColor ?? defaults.fillColor!.resolve(inactiveStates)!; final Set focusedStates = states..add(MaterialState.focused); - final Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) + Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates) ?? widget.focusColor ?? radioTheme.overlayColor?.resolve(focusedStates) - ?? themeData.focusColor; + ?? defaults.overlayColor!.resolve(focusedStates)!; final Set hoveredStates = states..add(MaterialState.hovered); - final Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) - ?? widget.hoverColor - ?? radioTheme.overlayColor?.resolve(hoveredStates) - ?? themeData.hoverColor; + Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates) + ?? widget.hoverColor + ?? radioTheme.overlayColor?.resolve(hoveredStates) + ?? defaults.overlayColor!.resolve(hoveredStates)!; final Set activePressedStates = activeStates..add(MaterialState.pressed); final Color effectiveActivePressedOverlayColor = widget.overlayColor?.resolve(activePressedStates) - ?? radioTheme.overlayColor?.resolve(activePressedStates) - ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); + ?? radioTheme.overlayColor?.resolve(activePressedStates) + ?? activeColor?.withAlpha(kRadialReactionAlpha) + ?? defaults.overlayColor!.resolve(activePressedStates)!; final Set inactivePressedStates = inactiveStates..add(MaterialState.pressed); final Color effectiveInactivePressedOverlayColor = widget.overlayColor?.resolve(inactivePressedStates) - ?? radioTheme.overlayColor?.resolve(inactivePressedStates) - ?? effectiveActiveColor.withAlpha(kRadialReactionAlpha); + ?? radioTheme.overlayColor?.resolve(inactivePressedStates) + ?? inactiveColor?.withAlpha(kRadialReactionAlpha) + ?? defaults.overlayColor!.resolve(inactivePressedStates)!; + + if (downPosition != null) { + effectiveHoverOverlayColor = states.contains(MaterialState.selected) + ? effectiveActivePressedOverlayColor + : effectiveInactivePressedOverlayColor; + effectiveFocusOverlayColor = states.contains(MaterialState.selected) + ? effectiveActivePressedOverlayColor + : effectiveInactivePressedOverlayColor; + } return Semantics( inMutuallyExclusiveGroup: true, @@ -485,3 +485,134 @@ class _RadioPainter extends ToggleablePainter { } } } + +// Hand coded defaults based on Material Design 2. +class _RadioDefaultsM2 extends RadioThemeData { + _RadioDefaultsM2(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + MaterialStateProperty get fillColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _theme.disabledColor; + } + if (states.contains(MaterialState.selected)) { + return _colors.secondary; + } + return _theme.unselectedWidgetColor; + }); + } + + @override + MaterialStateProperty get overlayColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.pressed)) { + return fillColor.resolve(states).withAlpha(kRadialReactionAlpha); + } + if (states.contains(MaterialState.focused)) { + return _theme.focusColor; + } + if (states.contains(MaterialState.hovered)) { + return _theme.hoverColor; + } + return Colors.transparent; + }); + } + + @override + MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize; + + @override + VisualDensity get visualDensity => _theme.visualDensity; +} + +// BEGIN GENERATED TOKEN PROPERTIES - Radio + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_132 + +class _RadioDefaultsM3 extends RadioThemeData { + _RadioDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + + @override + MaterialStateProperty get fillColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.primary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary; + } + if (states.contains(MaterialState.focused)) { + return _colors.primary; + } + return _colors.primary; + } + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.onSurface; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSurface; + } + if (states.contains(MaterialState.focused)) { + return _colors.onSurface; + } + return _colors.onSurfaceVariant; + }); + } + + @override + MaterialStateProperty get overlayColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + if (states.contains(MaterialState.pressed)) { + return _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(MaterialState.focused)) { + return _colors.primary.withOpacity(0.12); + } + return Colors.transparent; + } + if (states.contains(MaterialState.pressed)) { + return _colors.primary.withOpacity(0.12); + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSurface.withOpacity(0.08); + } + if (states.contains(MaterialState.focused)) { + return _colors.onSurface.withOpacity(0.12); + } + return Colors.transparent; + }); + } + + @override + MaterialTapTargetSize get materialTapTargetSize => _theme.materialTapTargetSize; + + @override + VisualDensity get visualDensity => _theme.visualDensity; +} + +// END GENERATED TOKEN PROPERTIES - Radio diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 52ef53b9fb..38f38c89b2 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1266,6 +1266,7 @@ class ThemeData with Diagnosticable { /// * Lists: [ListTile] /// * Navigation bar: [NavigationBar] (new, replacing [BottomNavigationBar]) /// * [Navigation rail](https://m3.material.io/components/navigation-rail): [NavigationRail] + /// * Radio button: [Radio] /// * Switch: [Switch] /// * Top app bar: [AppBar] /// diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart index f3080f8bf6..694b1de1f2 100644 --- a/packages/flutter/test/material/radio_test.dart +++ b/packages/flutter/test/material/radio_test.dart @@ -19,17 +19,22 @@ import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { + final ThemeData theme = ThemeData(); + testWidgets('Radio control test', (WidgetTester tester) async { final Key key = UniqueKey(); final List log = []; - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: 2, - onChanged: log.add, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: 2, + onChanged: log.add, + ), ), ), )); @@ -39,14 +44,17 @@ void main() { expect(log, equals([1])); log.clear(); - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: 1, - onChanged: log.add, - activeColor: Colors.green[500], + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: 1, + onChanged: log.add, + activeColor: Colors.green[500], + ), ), ), )); @@ -55,13 +63,16 @@ void main() { expect(log, isEmpty); - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: 2, - onChanged: null, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: 2, + onChanged: null, + ), ), ), )); @@ -75,14 +86,17 @@ void main() { final Key key = UniqueKey(); final List log = []; - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: 2, - onChanged: log.add, - toggleable: true, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: 2, + onChanged: log.add, + toggleable: true, + ), ), ), )); @@ -92,14 +106,17 @@ void main() { expect(log, equals([1])); log.clear(); - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: 1, - onChanged: log.add, - toggleable: true, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: 1, + onChanged: log.add, + toggleable: true, + ), ), ), )); @@ -109,14 +126,17 @@ void main() { expect(log, equals([null])); log.clear(); - await tester.pumpWidget(Material( - child: Center( - child: Radio( - key: key, - value: 1, - groupValue: null, - onChanged: log.add, - toggleable: true, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Center( + child: Radio( + key: key, + value: 1, + groupValue: null, + onChanged: log.add, + toggleable: true, + ), ), ), )); @@ -130,7 +150,7 @@ void main() { final Key key1 = UniqueKey(); await tester.pumpWidget( Theme( - data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded), + data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded), child: Directionality( textDirection: TextDirection.ltr, child: Material( @@ -152,7 +172,7 @@ void main() { final Key key2 = UniqueKey(); await tester.pumpWidget( Theme( - data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), + data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap), child: Directionality( textDirection: TextDirection.ltr, child: Material( @@ -176,11 +196,14 @@ void main() { testWidgets('Radio semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); - await tester.pumpWidget(Material( - child: Radio( - value: 1, - groupValue: 2, - onChanged: (int? i) { }, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Radio( + value: 1, + groupValue: 2, + onChanged: (int? i) { }, + ), ), )); @@ -202,11 +225,14 @@ void main() { ], ), ignoreRect: true, ignoreTransform: true)); - await tester.pumpWidget(Material( - child: Radio( - value: 2, - groupValue: 2, - onChanged: (int? i) { }, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Radio( + value: 2, + groupValue: 2, + onChanged: (int? i) { }, + ), ), )); @@ -229,11 +255,14 @@ void main() { ], ), ignoreRect: true, ignoreTransform: true)); - await tester.pumpWidget(const Material( - child: Radio( - value: 1, - groupValue: 2, - onChanged: null, + await tester.pumpWidget(Theme( + data: theme, + child: const Material( + child: Radio( + value: 1, + groupValue: 2, + onChanged: null, + ), ), )); @@ -267,11 +296,14 @@ void main() { ], ), ignoreRect: true, ignoreTransform: true)); - await tester.pumpWidget(const Material( - child: Radio( - value: 2, - groupValue: 2, - onChanged: null, + await tester.pumpWidget(Theme( + data: theme, + child: const Material( + child: Radio( + value: 2, + groupValue: 2, + onChanged: null, + ), ), )); @@ -301,14 +333,17 @@ void main() { semanticEvent = message; }); - await tester.pumpWidget(Material( - child: Radio( - key: key, - value: 1, - groupValue: radioValue, - onChanged: (int? i) { - radioValue = i; - }, + await tester.pumpWidget(Theme( + data: theme, + child: Material( + child: Radio( + key: key, + value: 1, + groupValue: radioValue, + onChanged: (int? i) { + radioValue = i; + }, + ), ), )); @@ -327,12 +362,12 @@ void main() { tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler(SystemChannels.accessibility, null); }); - testWidgets('Radio ink ripple is displayed correctly', (WidgetTester tester) async { + testWidgets('Radio ink ripple is displayed correctly - M2', (WidgetTester tester) async { final Key painterKey = UniqueKey(); const Key radioKey = Key('radio'); await tester.pumpWidget(MaterialApp( - theme: ThemeData(), + theme: ThemeData(useMaterial3: false), home: Scaffold( body: RepaintBoundary( key: painterKey, @@ -366,6 +401,7 @@ void main() { const double splashRadius = 30; Widget buildApp() { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -404,6 +440,7 @@ void main() { const Key radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -453,13 +490,15 @@ void main() { expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), - paints - ..rect( - color: const Color(0xffffffff), - rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), - ) - ..circle(color: Colors.orange[500]) - ..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0), + theme.useMaterial3 + ? (paints..rect()..circle(color: Colors.orange[500])..circle(color: theme.colorScheme.onSurface)) + : (paints + ..rect( + color: const Color(0xffffffff), + rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), + ) + ..circle(color: Colors.orange[500]) + ..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0)), ); // Check when the radio is selected, but disabled. @@ -485,6 +524,7 @@ void main() { const Key radioKey = Key('radio'); Widget buildApp({bool enabled = true}) { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -534,14 +574,14 @@ void main() { await tester.pump(); await tester.pumpAndSettle(); expect( - Material.of(tester.element(find.byKey(radioKey))), - paints - ..rect( - color: const Color(0xffffffff), - rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), - ) - ..circle(color: Colors.orange[500]) - ..circle(color: const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0), + Material.of(tester.element(find.byKey(radioKey))), + paints + ..rect( + color: const Color(0xffffffff), + rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), + ) + ..circle(color: Colors.orange[500]) + ..circle(color: theme.useMaterial3 ? theme.colorScheme.onSurface : const Color(0x8a000000), style: PaintingStyle.stroke, strokeWidth: 2.0), ); // Check when the radio is selected, but disabled. @@ -570,6 +610,7 @@ void main() { final FocusNode focusNode2 = FocusNode(debugLabel: 'radio2'); Widget buildApp({bool enabled = true}) { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -644,6 +685,7 @@ void main() { Future buildTest(VisualDensity visualDensity) async { return tester.pumpWidget( MaterialApp( + theme: theme, home: Material( child: Center( child: Radio( @@ -682,6 +724,7 @@ void main() { // Test Radio() constructor await tester.pumpWidget( MaterialApp( + theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, @@ -713,6 +756,7 @@ void main() { // Test default cursor await tester.pumpWidget( MaterialApp( + theme: theme, home: Scaffold( body: Align( alignment: Alignment.topLeft, @@ -735,8 +779,9 @@ void main() { // Test default cursor when disabled await tester.pumpWidget( - const MaterialApp( - home: Scaffold( + MaterialApp( + theme: theme, + home: const Scaffold( body: Align( alignment: Alignment.topLeft, child: Material( @@ -783,6 +828,7 @@ void main() { const Key radioKey = Key('radio'); Widget buildApp({required bool enabled}) { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -890,6 +936,7 @@ void main() { const Key radioKey = Key('radio'); Widget buildApp() { return MaterialApp( + theme: theme, home: Material( child: Center( child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { @@ -922,13 +969,15 @@ void main() { expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byKey(radioKey))), - paints - ..rect( - color: const Color(0xffffffff), - rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), - ) - ..circle(color: Colors.black12) - ..circle(color: focusedFillColor), + theme.useMaterial3 + ? (paints..rect()..circle(color: theme.colorScheme.primary.withOpacity(0.12))..circle(color: focusedFillColor)) + : (paints + ..rect( + color: const Color(0xffffffff), + rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), + ) + ..circle(color: Colors.black12) + ..circle(color: focusedFillColor)), ); // Start hovering @@ -944,7 +993,7 @@ void main() { color: const Color(0xffffffff), rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), ) - ..circle(color: Colors.black12) + ..circle(color: theme.useMaterial3 ? theme.colorScheme.primary.withOpacity(0.08) : Colors.black12) ..circle(color: hoveredFillColor), ); }); @@ -988,6 +1037,7 @@ void main() { Widget buildRadio({bool active = false, bool focused = false, bool useOverlay = true}) { return MaterialApp( + theme: theme, home: Scaffold( body: Radio( focusNode: focusNode, @@ -1061,6 +1111,7 @@ void main() { reason: 'Active pressed Radio should have overlay color: $activePressedOverlayColor', ); + await tester.pumpWidget(Container()); await tester.pumpWidget(buildRadio(focused: true)); await tester.pumpAndSettle(); @@ -1097,6 +1148,7 @@ void main() { Widget buildRadio(bool show) { return MaterialApp( + theme: theme, home: Material( child: Center( child: show ? Radio(key: key, value: true, groupValue: false, onChanged: (_) { }) : Container(), @@ -1121,8 +1173,9 @@ void main() { const String longPressTooltip = 'long press tooltip'; const String tapTooltip = 'tap tooltip'; await tester.pumpWidget( - const MaterialApp( - home: Material( + MaterialApp( + theme: theme, + home: const Material( child: Tooltip( message: longPressTooltip, child: Radio(value: true, groupValue: false, onChanged: null), @@ -1149,8 +1202,9 @@ void main() { // Tooltip shows up after tapping when set triggerMode to TooltipTriggerMode.tap. await tester.pumpWidget( - const MaterialApp( - home: Material( + MaterialApp( + theme: theme, + home: const Material( child: Tooltip( triggerMode: TooltipTriggerMode.tap, message: tapTooltip, @@ -1167,4 +1221,154 @@ void main() { await tester.pump(const Duration(milliseconds: 10)); expect(find.text(tapTooltip), findsOneWidget); }); + + testWidgets('Radio button default colors', (WidgetTester tester) async { + Widget buildRadio({bool enabled = true, bool selected = true}) { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Radio( + value: true, + groupValue: true, + onChanged: enabled ? (_) {} : null, + ), + ) + ); + } + + await tester.pumpWidget(buildRadio()); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + paints + ..circle(color: const Color(0xFF2196F3)) // Outer circle - blue primary value + ..circle(color: const Color(0xFF2196F3))..restore(), // Inner circle - blue primary value + ); + + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildRadio(selected: false)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + paints + ..save() + ..circle(color: const Color(0xFF2196F3)) + ..restore(), + ); + + await tester.pumpWidget(Container()); + await tester.pumpWidget(buildRadio(enabled: false)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + theme.useMaterial3 + ? (paints + ..circle(color: theme.colorScheme.onSurface.withOpacity(0.38))) + : (paints..circle(color: Colors.black38)) + ); + }); + + testWidgets('Radio button default overlay colors in hover/focus/press states', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'Radio'); + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + + final ColorScheme colors = theme.colorScheme; + final bool material3 = theme.useMaterial3; + Widget buildRadio({bool enabled = true, bool focused = false, bool selected = true}) { + return MaterialApp( + theme: theme, + home: Scaffold( + body: Radio( + focusNode: focusNode, + autofocus: focused, + value: true, + groupValue: selected, + onChanged: enabled ? (_) {} : null, + ), + ), + ); + } + + // default selected radio + await tester.pumpWidget(buildRadio()); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Radio))), + material3 + ? (paints..circle(color: colors.primary.withOpacity(1))) + : (paints..circle(color: colors.secondary)) + ); + + // selected radio in pressed state + await tester.pumpWidget(buildRadio()); + await tester.startGesture(tester.getCenter(find.byType(Radio))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + paints..circle(color: material3 + ? colors.onSurface.withOpacity(0.12) + : colors.secondary.withAlpha(0x1F)) + ..circle(color: material3 + ? colors.primary.withOpacity(1) + : colors.secondary + ) + ); + + // unselected radio in pressed state + await tester.pumpWidget(buildRadio(selected: false)); + await tester.startGesture(tester.getCenter(find.byType(Radio))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + material3 + ? (paints..circle(color: colors.primary.withOpacity(0.12))..circle(color: colors.onSurface.withOpacity(1))) + : (paints..circle(color: theme.unselectedWidgetColor.withAlpha(0x1F))..circle(color: theme.unselectedWidgetColor)) + ); + + // selected radio in focused state + await tester.pumpWidget(Container()); // reset test + await tester.pumpWidget(buildRadio(focused: true)); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + + expect( + Material.of(tester.element(find.byType(Radio))), + material3 + ? (paints..circle(color: colors.primary.withOpacity(0.12))..circle(color: colors.primary.withOpacity(1))) + : (paints..circle(color: theme.focusColor)..circle(color: colors.secondary)) + ); + + // unselected radio in focused state + await tester.pumpWidget(Container()); // reset test + await tester.pumpWidget(buildRadio(focused: true, selected: false)); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + + expect( + Material.of(tester.element(find.byType(Radio))), + material3 + ? (paints..circle(color: colors.onSurface.withOpacity(0.12))..circle(color: colors.onSurface.withOpacity(1))) + : (paints..circle(color: theme.focusColor)..circle(color: theme.unselectedWidgetColor)) + ); + + // selected radio in hovered state + await tester.pumpWidget(Container()); // reset test + await tester.pumpWidget(buildRadio()); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(Radio))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Radio))), + material3 + ? (paints..circle(color: colors.primary.withOpacity(0.08))..circle(color: colors.primary.withOpacity(1))) + : (paints..circle(color: theme.hoverColor)..circle(color: colors.secondary)) + ); + }); }