From 60a072b0989efd40c4b7ce8b5741d40a20228f0b Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 22 Jul 2021 17:18:44 -0700 Subject: [PATCH] Added Checkbox support for MaterialStateBorderSide (#86910) --- .../flutter/lib/src/material/checkbox.dart | 49 ++++++-- .../flutter/test/material/checkbox_test.dart | 116 ++++++++++++++++++ 2 files changed, 152 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index 5632c3b5728..b54b46e33c0 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -310,7 +310,23 @@ class Checkbox extends StatefulWidget { final OutlinedBorder? shape; /// {@template flutter.material.checkbox.side} - /// The side of the checkbox's border. + /// The color and width of the checkbox's border. + /// + /// This property can be a [MaterialStateBorderSide] that can + /// specify different border color and widths depending on the + /// checkbox's state. + /// + /// Resolves in the following states: + /// * [MaterialState.pressed]. + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// + /// If this property is not a [MaterialStateBorderSide] and it is + /// non-null, then it is only rendered when the checkbox's value is + /// false. The difference in interpretation is for backwards + /// compatibility. /// {@endtemplate} /// /// If this property is null then [CheckboxThemeData.side] of [ThemeData.checkboxTheme] @@ -383,6 +399,14 @@ class _CheckboxState extends State with TickerProviderStateMixin, Togg }); } + BorderSide? _resolveSide(BorderSide? side) { + if (side is MaterialStateBorderSide) + return MaterialStateProperty.resolveAs(side, states); + if (!states.contains(MaterialState.selected)) + return side; + return null; + } + @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -477,7 +501,7 @@ class _CheckboxState extends State with TickerProviderStateMixin, Togg ..shape = widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(1.0)), ) - ..side = widget.side ?? themeData.checkboxTheme.side, + ..side = _resolveSide(widget.side) ?? _resolveSide(themeData.checkboxTheme.side), ), ); } @@ -563,13 +587,13 @@ class _CheckboxPainter extends ToggleablePainter { ..strokeWidth = _kStrokeWidth; } - void _drawBorder(Canvas canvas, Rect outer, double t, Paint paint) { - assert(t >= 0.0 && t <= 0.5); - OutlinedBorder resolvedShape = shape; - if (side == null) { - resolvedShape = resolvedShape.copyWith(side: BorderSide(width: 2, color: paint.color)); + void _drawBox(Canvas canvas, Rect outer, Paint paint, BorderSide? side, bool fill) { + if (fill) { + canvas.drawPath(shape.getOuterPath(outer), paint); + } + if (side != null) { + shape.copyWith(side: side).paint(canvas, outer); } - resolvedShape.copyWith(side: side).paint(canvas, outer); } void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) { @@ -622,14 +646,13 @@ class _CheckboxPainter extends ToggleablePainter { if (previousValue == false || value == false) { final double t = value == false ? 1.0 - tNormalized : tNormalized; final Rect outer = _outerRectAt(origin, t); - final Path emptyCheckboxPath = shape.copyWith(side: side).getOuterPath(outer); final Paint paint = Paint()..color = _colorAt(t); if (t <= 0.5) { - _drawBorder(canvas, outer, t, paint); + final BorderSide border = side ?? BorderSide(width: 2, color: paint.color); + _drawBox(canvas, outer, paint, border, false); // only paint the border } else { - canvas.drawPath(emptyCheckboxPath, paint); - + _drawBox(canvas, outer, paint, side, true); final double tShrink = (t - 0.5) * 2.0; if (previousValue == null || value == null) _drawDash(canvas, origin, tShrink, strokePaint); @@ -639,8 +662,8 @@ class _CheckboxPainter extends ToggleablePainter { } else { // Two cases: null to true, true to null final Rect outer = _outerRectAt(origin, 1.0); final Paint paint = Paint() ..color = _colorAt(1.0); - canvas.drawPath(shape.copyWith(side: side).getOuterPath(outer), paint); + _drawBox(canvas, outer, paint, side, true); if (tNormalized <= 0.5) { final double tShrink = 1.0 - tNormalized * 2.0; if (previousValue == true) diff --git a/packages/flutter/test/material/checkbox_test.dart b/packages/flutter/test/material/checkbox_test.dart index 6c03f41aad4..e390e85a3aa 100644 --- a/packages/flutter/test/material/checkbox_test.dart +++ b/packages/flutter/test/material/checkbox_test.dart @@ -1193,6 +1193,122 @@ void main() { // Release pointer after widget disappeared. await gesture.up(); }); + + testWidgets('Checkbox BorderSide side only applies when unselected', (WidgetTester tester) async { + const Color borderColor = Color(0xfff44336); + const Color activeColor = Color(0xff123456); + const BorderSide side = BorderSide( + width: 4, + color: borderColor, + ); + + Widget buildApp({ bool? value, bool enabled = true }) { + return MaterialApp( + home: Material( + child: Center( + child: Checkbox( + value: value, + tristate: value == null, + activeColor: activeColor, + onChanged: enabled ? (bool? newValue) { } : null, + side: side, + ), + ), + ), + ); + } + + RenderBox getCheckboxRenderer() { + return tester.renderObject(find.byType(Checkbox)); + } + + void expectBorder() { + expect( + getCheckboxRenderer(), + paints + ..drrect( + color: borderColor, + outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)), + inner: RRect.fromLTRBR(19, 19, 29, 29, const Radius.circular(-3)), + ), + ); + } + + // Checkbox is unselected, so the specified BorderSide appears. + + await tester.pumpWidget(buildApp(value: false)); + await tester.pumpAndSettle(); + expectBorder(); + + await tester.pumpWidget(buildApp(value: false, enabled: false)); + await tester.pumpAndSettle(); + expectBorder(); + + // Checkbox is selected/interdeterminate, so the specified BorderSide + // does not appear. + + await tester.pumpWidget(buildApp(value: true)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), isNot(paints..drrect())); // no border + expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill + + await tester.pumpWidget(buildApp(value: null)); + await tester.pumpAndSettle(); + expect(getCheckboxRenderer(), isNot(paints..drrect())); // no border + expect(getCheckboxRenderer(), paints..path(color: activeColor)); // checkbox fill + }); + + testWidgets('Checkbox MaterialStateBorderSide applies unconditionally', (WidgetTester tester) async { + const Color borderColor = Color(0xfff44336); + const BorderSide side = BorderSide( + width: 4, + color: borderColor, + ); + + Widget buildApp({ bool? value, bool enabled = true }) { + return MaterialApp( + home: Material( + child: Center( + child: Checkbox( + value: value, + tristate: value == null, + onChanged: enabled ? (bool? newValue) { } : null, + side: MaterialStateBorderSide.resolveWith((Set states) => side), + ), + ), + ), + ); + } + + void expectBorder() { + expect( + tester.renderObject(find.byType(Checkbox)), + paints + ..drrect( + color: borderColor, + outer: RRect.fromLTRBR(15, 15, 33, 33, const Radius.circular(1)), + inner: RRect.fromLTRBR(19, 19, 29, 29, const Radius.circular(-3)), + ), + ); + } + + await tester.pumpWidget(buildApp(value: false)); + await tester.pumpAndSettle(); + expectBorder(); + + + await tester.pumpWidget(buildApp(value: false, enabled: false)); + await tester.pumpAndSettle(); + expectBorder(); + + await tester.pumpWidget(buildApp(value: true)); + await tester.pumpAndSettle(); + expectBorder(); + + await tester.pumpWidget(buildApp(value: null)); + await tester.pumpAndSettle(); + expectBorder(); + }); } class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {