diff --git a/packages/flutter/lib/src/cupertino/checkbox.dart b/packages/flutter/lib/src/cupertino/checkbox.dart index 51fb59ff27..76d4f14882 100644 --- a/packages/flutter/lib/src/cupertino/checkbox.dart +++ b/packages/flutter/lib/src/cupertino/checkbox.dart @@ -104,6 +104,7 @@ class CupertinoCheckbox extends StatefulWidget { required this.onChanged, this.activeColor, this.inactiveColor, + this.fillColor, this.checkColor, this.focusColor, this.focusNode, @@ -152,11 +153,51 @@ class CupertinoCheckbox extends StatefulWidget { /// The color to use when this checkbox is checked. /// + /// If [fillColor] returns a non-null color in the [WidgetState.selected] + /// state, [fillColor] will be used instead of [activeColor]. + /// /// Defaults to [CupertinoColors.activeBlue]. final Color? activeColor; + /// {@template flutter.cupertino.CupertinoCheckbox.fillColor} + /// The color used to fill this checkbox. + /// + /// Resolves in the following states: + /// * [WidgetState.selected]. + /// * [WidgetState.hovered]. + /// * [WidgetState.focused]. + /// * [WidgetState.disabled]. + /// + /// {@tool snippet} + /// This example resolves the [fillColor] based on the current [WidgetState] + /// of the [CupertinoCheckbox], providing a different [Color] when it is + /// [WidgetState.disabled]. + /// + /// ```dart + /// CupertinoCheckbox( + /// value: true, + /// onChanged: (_){}, + /// fillColor: WidgetStateProperty.resolveWith((Set states) { + /// if (states.contains(WidgetState.disabled)) { + /// return Colors.orange.withOpacity(.32); + /// } + /// return Colors.orange; + /// }) + /// ) + /// ``` + /// {@end-tool} + /// {@endtemplate} + /// + /// If [fillColor] resolves to null for the requested state, then the fill color + /// falls back to [activeColor] if the state includes [WidgetState.selected], + /// or [inactiveColor] otherwise. + final WidgetStateProperty? fillColor; + /// The color used if the checkbox is inactive. /// + /// If [fillColor] returns a non-null color in the unselected + /// state, [fillColor] will be used instead of [inactiveColor]. + /// /// By default, [CupertinoColors.inactiveGray] is used. final Color? inactiveColor; @@ -318,6 +359,7 @@ class _CupertinoCheckboxState extends State with TickerProvid @override Widget build(BuildContext context) { // Colors need to be resolved in selected and non selected states separately. + // The `states` getter constructs a new set every time, making it safe to edit in place. final Set activeStates = states..add(WidgetState.selected); final Set inactiveStates = states..remove(WidgetState.selected); @@ -325,7 +367,11 @@ class _CupertinoCheckboxState extends State with TickerProvid // throughout the lifecycle of this build method. final Set currentStates = states; - final Color effectiveActiveColor = _defaultFillColor.resolve(activeStates); + final Color effectiveActiveColor = widget.fillColor?.resolve(activeStates) + ?? _defaultFillColor.resolve(activeStates); + + final Color effectiveInactiveColor = widget.fillColor?.resolve(inactiveStates) + ?? _defaultFillColor.resolve(inactiveStates); final BorderSide effectiveBorderSide = _resolveSide(widget.side, currentStates) ?? _defaultSide.resolve(currentStates); @@ -353,7 +399,7 @@ class _CupertinoCheckboxState extends State with TickerProvid ..isFocused = currentStates.contains(WidgetState.focused) ..isHovered = currentStates.contains(WidgetState.hovered) ..activeColor = effectiveActiveColor - ..inactiveColor = _defaultFillColor.resolve(inactiveStates) + ..inactiveColor = effectiveInactiveColor ..checkColor = _defaultCheckColor.resolve(currentStates) ..value = value ..previousValue = _previousValue diff --git a/packages/flutter/test/cupertino/checkbox_test.dart b/packages/flutter/test/cupertino/checkbox_test.dart index 30e0867652..052081baba 100644 --- a/packages/flutter/test/cupertino/checkbox_test.dart +++ b/packages/flutter/test/cupertino/checkbox_test.dart @@ -7,6 +7,7 @@ // machines. @Tags(['reduced-test-set']) library; +import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -575,6 +576,134 @@ void main() { ); }); + testWidgets('Checkbox fill color resolves in enabled/disabled states', (WidgetTester tester) async { + const Color activeEnabledFillColor = Color(0xFF000001); + const Color activeDisabledFillColor = Color(0xFF000002); + + Color getFillColor(Set states) { + if (states.contains(WidgetState.disabled)) { + return activeDisabledFillColor; + } + return activeEnabledFillColor; + } + + final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); + + Widget buildApp({required bool enabled}) { + return CupertinoApp( + home: CupertinoCheckbox( + value: true, + fillColor: fillColor, + onChanged: enabled ? (bool? value) { } : null, + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject(find.byType(CupertinoCheckbox)); + } + + await tester.pumpWidget(buildApp(enabled: true)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeEnabledFillColor)); + + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor)); + }); + + testWidgets('Checkbox fill color take precedence over active/inactive colors', (WidgetTester tester) async { + const Color activeEnabledFillColor = Color(0xFF000001); + const Color activeDisabledFillColor = Color(0xFF000002); + const Color activeColor = Color(0xFF000003); + const Color inactiveColor = Color(0xFF000004); + + Color getFillColor(Set states) { + if (states.contains(WidgetState.disabled)) { + return activeDisabledFillColor; + } + return activeEnabledFillColor; + } + + final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); + + Widget buildApp({required bool enabled}) { + return CupertinoApp( + home: CupertinoCheckbox( + value: true, + fillColor: fillColor, + activeColor: activeColor, + inactiveColor: inactiveColor, + onChanged: enabled ? (bool? value) { } : null, + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject(find.byType(CupertinoCheckbox)); + } + + await tester.pumpWidget(buildApp(enabled: true)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeEnabledFillColor)); + + await tester.pumpWidget(buildApp(enabled: false)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor)); + }); + + testWidgets('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'checkbox'); + addTearDown(focusNode.dispose); + + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color hoveredFillColor = Color(0xFF000001); + const Color focusedFillColor = Color(0xFF000002); + const Color transparentColor = Color(0x00000000); + + Color getFillColor(Set states) { + if (states.contains(WidgetState.hovered)) { + return hoveredFillColor; + } + if (states.contains(WidgetState.focused)) { + return focusedFillColor; + } + return transparentColor; + } + + final WidgetStateProperty fillColor = WidgetStateColor.resolveWith(getFillColor); + + Widget buildApp({required bool enabled}) { + return CupertinoApp( + home: CupertinoCheckbox( + focusNode: focusNode, + value: enabled, + fillColor: fillColor, + onChanged: enabled ? (bool? value) { } : null, + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject(find.byType(CupertinoCheckbox)); + } + + await tester.pumpWidget(buildApp(enabled: true)); + focusNode.requestFocus(); + await tester.pumpAndSettle(); + expect(focusNode.hasPrimaryFocus, isTrue); + expect(getCheckboxRenderer(), paints..path(color: focusedFillColor)); + + // Start hovering. + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(CupertinoCheckbox))); + await tester.pumpAndSettle(); + + expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor)); + }); + testWidgets('Checkbox configures focus color', (WidgetTester tester) async { const Color defaultCheckColor = Color(0xffffffff); const Color defaultActiveFillColor = Color(0xff007aff);