Unverified Commit a433f88d authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

`Checkbox.fillColor` should be applied to checkbox's background color when it...

`Checkbox.fillColor` should be applied to checkbox's background color when it is unchecked. (#125643)
parent 50f83fc2
......@@ -20,27 +20,49 @@ class _${blockName}DefaultsM3 extends CheckboxThemeData {
final ColorScheme _colors;
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
MaterialStateBorderSide? get side {
return MaterialStateBorderSide.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return ${componentColor('md.comp.checkbox.selected.disabled.container')};
}
if (states.contains(MaterialState.error)) {
return ${componentColor('md.comp.checkbox.unselected.error.outline')};
if (states.contains(MaterialState.selected)) {
return const BorderSide(width: ${tokens['md.comp.checkbox.unselected.disabled.outline.width']}, color: Colors.transparent);
}
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.disabled.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.disabled.outline')}.withOpacity(${tokens['md.comp.checkbox.unselected.disabled.container.opacity']}));
}
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.checkbox.selected.container')};
return const BorderSide(width: ${tokens['md.comp.checkbox.selected.outline.width']}, color: Colors.transparent);
}
if (states.contains(MaterialState.error)) {
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.disabled.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.error.outline')});
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.checkbox.unselected.pressed.outline')};
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.pressed.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.pressed.outline')});
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.checkbox.unselected.hover.outline')};
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.hover.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.hover.outline')});
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.checkbox.unselected.focus.outline')};
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.focus.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.focus.outline')});
}
return BorderSide(width: ${tokens['md.comp.checkbox.unselected.outline.width']}, color: ${componentColor('md.comp.checkbox.unselected.outline')});
});
}
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.checkbox.selected.disabled.container')};
}
return Colors.transparent;
}
return ${componentColor('md.comp.checkbox.unselected.outline')};
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.error)) {
return ${componentColor('md.comp.checkbox.selected.error.container')};
}
return ${componentColor('md.comp.checkbox.selected.container')};
}
return Colors.transparent;
});
}
......
......@@ -104,7 +104,7 @@ class Checkbox extends StatefulWidget {
/// design [Checkbox].
///
/// If a [CupertinoCheckbox] is created, the following parameters are ignored:
/// [mouseCursor], [hoverColor], [overlayColor], [splashRadius],
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
/// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and
/// [side] will still affect the [CupertinoCheckbox] and should be handled if
/// native fidelity is important.
......@@ -452,10 +452,9 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
});
}
BorderSide? _resolveSide(BorderSide? side) {
BorderSide? _resolveSide(BorderSide? side, Set<MaterialState> states) {
if (side is MaterialStateBorderSide) {
final Set<MaterialState> sideStates = widget.isError ? (states..add(MaterialState.error)) : states;
return MaterialStateProperty.resolveAs<BorderSide?>(side, sideStates);
return MaterialStateProperty.resolveAs<BorderSide?>(side, states);
}
if (!states.contains(MaterialState.selected)) {
return side;
......@@ -521,10 +520,12 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
});
// Colors need to be resolved in selected and non selected states separately
// so that they can be lerped between.
final Set<MaterialState> errorState = states..add(MaterialState.error);
final Set<MaterialState> activeStates = widget.isError ? (errorState..add(MaterialState.selected)) : states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = widget.isError ? (errorState..remove(MaterialState.selected)) : states..remove(MaterialState.selected);
final Set<MaterialState> activeStates = states..add(MaterialState.selected);
final Set<MaterialState> inactiveStates = states..remove(MaterialState.selected);
if (widget.isError) {
activeStates.add(MaterialState.error);
inactiveStates.add(MaterialState.error);
}
final Color? activeColor = widget.fillColor?.resolve(activeStates)
?? _widgetFillColor.resolve(activeStates)
?? checkboxTheme.fillColor?.resolve(activeStates);
......@@ -536,13 +537,26 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
final Color effectiveInactiveColor = inactiveColor
?? defaults.fillColor!.resolve(inactiveStates)!;
final Set<MaterialState> focusedStates = widget.isError ? (errorState..add(MaterialState.focused)) : states..add(MaterialState.focused);
final BorderSide activeSide = _resolveSide(widget.side, activeStates)
?? _resolveSide(checkboxTheme.side, activeStates)
?? _resolveSide(defaults.side, activeStates)!;
final BorderSide inactiveSide = _resolveSide(widget.side, inactiveStates)
?? _resolveSide(checkboxTheme.side, inactiveStates)
?? _resolveSide(defaults.side, inactiveStates)!;
final Set<MaterialState> focusedStates = states..add(MaterialState.focused);
if (widget.isError) {
focusedStates.add(MaterialState.error);
}
Color effectiveFocusOverlayColor = widget.overlayColor?.resolve(focusedStates)
?? widget.focusColor
?? checkboxTheme.overlayColor?.resolve(focusedStates)
?? defaults.overlayColor!.resolve(focusedStates)!;
final Set<MaterialState> hoveredStates = widget.isError ? (errorState..add(MaterialState.hovered)) : states..add(MaterialState.hovered);
final Set<MaterialState> hoveredStates = states..add(MaterialState.hovered);
if (widget.isError) {
hoveredStates.add(MaterialState.error);
}
Color effectiveHoverOverlayColor = widget.overlayColor?.resolve(hoveredStates)
?? widget.hoverColor
?? checkboxTheme.overlayColor?.resolve(hoveredStates)
......@@ -606,7 +620,8 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
..value = value
..previousValue = _previousValue
..shape = widget.shape ?? checkboxTheme.shape ?? defaults.shape!
..side = _resolveSide(widget.side) ?? _resolveSide(checkboxTheme.side),
..activeSide = activeSide
..inactiveSide = inactiveSide,
),
);
}
......@@ -656,13 +671,23 @@ class _CheckboxPainter extends ToggleablePainter {
notifyListeners();
}
BorderSide? get side => _side;
BorderSide? _side;
set side(BorderSide? value) {
if (_side == value) {
BorderSide get activeSide => _activeSide!;
BorderSide? _activeSide;
set activeSide(BorderSide value) {
if (_activeSide == value) {
return;
}
_side = value;
_activeSide = value;
notifyListeners();
}
BorderSide get inactiveSide => _inactiveSide!;
BorderSide? _inactiveSide;
set inactiveSide(BorderSide value) {
if (_inactiveSide == value) {
return;
}
_inactiveSide = value;
notifyListeners();
}
......@@ -677,8 +702,7 @@ class _CheckboxPainter extends ToggleablePainter {
return rect;
}
// The checkbox's border color if value == false, or its fill color when
// value == true or null.
// The checkbox's fill color
Color _colorAt(double t) {
// As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
return t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0)!;
......@@ -692,10 +716,8 @@ class _CheckboxPainter extends ToggleablePainter {
..strokeWidth = _kStrokeWidth;
}
void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side, bool fill) {
if (fill) {
canvas.drawPath(shape.getOuterPath(outer), paint);
}
void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side) {
canvas.drawPath(shape.getOuterPath(outer), paint);
if (side != null) {
shape.copyWith(side: side).paint(canvas, outer);
}
......@@ -754,10 +776,10 @@ class _CheckboxPainter extends ToggleablePainter {
final Paint paint = Paint()..color = _colorAt(t);
if (t <= 0.5) {
final BorderSide border = side ?? BorderSide(width: 2, color: paint.color);
_drawBox(canvas, outer, paint, border, false); // only paint the border
final BorderSide border = BorderSide.lerp(inactiveSide, activeSide, t);
_drawBox(canvas, outer, paint, border);
} else {
_drawBox(canvas, outer, paint, side, true);
_drawBox(canvas, outer, paint, activeSide);
final double tShrink = (t - 0.5) * 2.0;
if (previousValue == null || value == null) {
_drawDash(canvas, origin, tShrink, strokePaint);
......@@ -769,7 +791,7 @@ class _CheckboxPainter extends ToggleablePainter {
final Rect outer = _outerRectAt(origin, 1.0);
final Paint paint = Paint() ..color = _colorAt(1.0);
_drawBox(canvas, outer, paint, side, true);
_drawBox(canvas, outer, paint, activeSide);
if (tNormalized <= 0.5) {
final double tShrink = 1.0 - tNormalized * 2.0;
if (previousValue ?? false) {
......@@ -798,16 +820,35 @@ class _CheckboxDefaultsM2 extends CheckboxThemeData {
final ThemeData _theme;
final ColorScheme _colors;
@override
MaterialStateBorderSide? get side {
return MaterialStateBorderSide.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return const BorderSide(width: 2.0, color: Colors.transparent);
}
return BorderSide(width: 2.0, color: _theme.disabledColor);
}
if (states.contains(MaterialState.selected)) {
return const BorderSide(width: 2.0, color: Colors.transparent);
}
return BorderSide(width: 2.0, color: _theme.unselectedWidgetColor);
});
}
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _theme.disabledColor;
if (states.contains(MaterialState.selected)) {
return _theme.disabledColor;
}
return Colors.transparent;
}
if (states.contains(MaterialState.selected)) {
return _colors.secondary;
}
return _theme.unselectedWidgetColor;
return Colors.transparent;
});
}
......@@ -865,27 +906,49 @@ class _CheckboxDefaultsM3 extends CheckboxThemeData {
final ColorScheme _colors;
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
MaterialStateBorderSide? get side {
return MaterialStateBorderSide.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.error)) {
return _colors.error;
if (states.contains(MaterialState.selected)) {
return const BorderSide(width: 2.0, color: Colors.transparent);
}
return BorderSide(width: 2.0, color: _colors.onSurface.withOpacity(0.38));
}
if (states.contains(MaterialState.selected)) {
return _colors.primary;
return const BorderSide(width: 0.0, color: Colors.transparent);
}
if (states.contains(MaterialState.error)) {
return BorderSide(width: 2.0, color: _colors.error);
}
if (states.contains(MaterialState.pressed)) {
return _colors.onSurface;
return BorderSide(width: 2.0, color: _colors.onSurface);
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurface;
return BorderSide(width: 2.0, color: _colors.onSurface);
}
if (states.contains(MaterialState.focused)) {
return _colors.onSurface;
return BorderSide(width: 2.0, color: _colors.onSurface);
}
return _colors.onSurfaceVariant;
return BorderSide(width: 2.0, color: _colors.onSurfaceVariant);
});
}
@override
MaterialStateProperty<Color> get fillColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
if (states.contains(MaterialState.selected)) {
return _colors.onSurface.withOpacity(0.38);
}
return Colors.transparent;
}
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.error)) {
return _colors.error;
}
return _colors.primary;
}
return Colors.transparent;
});
}
......
......@@ -438,17 +438,21 @@ void main() {
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), isNot(paints..path())); // checkmark is rendered as a path
expect(getCheckboxRenderer(), paints..path(color: Colors.transparent)); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path()); // checkmark is rendered as a path
expect(getCheckboxRenderer(),
paints
..path(color: theme.useMaterial3 ? theme.colorScheme.primary : theme.colorScheme.secondary)
..path(color: theme.useMaterial3 ? theme.colorScheme.onPrimary : const Color(0xFFFFFFFF))
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), isNot(paints..path())); // checkmark is rendered as a path
expect(getCheckboxRenderer(), paints..path(color: Colors.transparent)); // paint transparent border
expect(getCheckboxRenderer(), isNot(paints..line())); // null is rendered as a line (a "dash")
expect(getCheckboxRenderer(), paints..drrect()); // empty checkbox
......@@ -458,7 +462,11 @@ void main() {
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path()); // checkmark is rendered as a path
expect(getCheckboxRenderer(),
paints
..path(color: theme.useMaterial3 ? theme.colorScheme.primary : theme.colorScheme.secondary)
..path(color: theme.useMaterial3 ? theme.colorScheme.onPrimary : const Color(0xFFFFFFFF))
); // checkmark is rendered as a path
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
......@@ -1493,17 +1501,16 @@ void main() {
await tester.pumpAndSettle();
expectBorder();
// Checkbox is selected/indeterminate, so the specified BorderSide
// does not appear.
// Checkbox is selected/indeterminate, so the specified BorderSide is transparent
await tester.pumpWidget(buildApp(value: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), isNot(paints..drrect())); // no border
expect(getCheckboxRenderer(), paints..drrect(color: Colors.transparent));
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), isNot(paints..drrect())); // no border
expect(getCheckboxRenderer(), paints..drrect(color: Colors.transparent));
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill
});
......@@ -1827,6 +1834,59 @@ void main() {
expect(find.byType(CupertinoCheckbox), findsNothing);
}
});
testWidgets('Checkbox respects fillColor when it is unchecked', (WidgetTester tester) async {
const Color activeBackgroundColor = Color(0xff123456);
const Color inactiveBackgroundColor = Color(0xff654321);
Widget buildApp({ bool enabled = true }) {
return MaterialApp(
theme: theme,
home: Material(
child: Center(
child: Checkbox(
fillColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return activeBackgroundColor;
}
return inactiveBackgroundColor;
}),
value: false,
onChanged: enabled ? (bool? newValue) { } : null,
),
),
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
// Checkbox is unselected, so the default BorderSide appears and fillColor is checkbox's background color.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..drrect(
color: theme.useMaterial3 ? theme.colorScheme.onSurfaceVariant : theme.unselectedWidgetColor,
),
);
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
getCheckboxRenderer(),
paints
..drrect(
color: theme.useMaterial3 ? theme.colorScheme.onSurface.withOpacity(0.38) : theme.disabledColor,
),
);
expect(getCheckboxRenderer(), paints..path(color: inactiveBackgroundColor));
});
}
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
......
......@@ -142,7 +142,7 @@ void main() {
// Checkbox.
await tester.pumpWidget(buildCheckbox());
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..drrect(color: defaultFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: defaultFillColor));
// Size from MaterialTapTargetSize.shrinkWrap with added VisualDensity.
expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0) + visualDensity.baseSizeAdjustment);
......@@ -241,7 +241,7 @@ void main() {
// Checkbox.
await tester.pumpWidget(buildCheckbox());
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..drrect(color: defaultFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: defaultFillColor));
// Size from MaterialTapTargetSize.shrinkWrap with added VisualDensity.
expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0) + visualDensity.baseSizeAdjustment);
......@@ -294,7 +294,7 @@ void main() {
// Unselected checkbox.
await tester.pumpWidget(buildCheckbox());
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..drrect(color: themeDefaultFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: themeDefaultFillColor));
// Selected checkbox.
await tester.pumpWidget(buildCheckbox(selected: true));
......
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