Added Checkbox support for MaterialStateBorderSide (#86910)

This commit is contained in:
Hans Muller 2021-07-22 17:18:44 -07:00 committed by GitHub
parent 6d8ab062a2
commit 60a072b098
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 152 additions and 13 deletions

View file

@ -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<Checkbox> with TickerProviderStateMixin, Togg
});
}
BorderSide? _resolveSide(BorderSide? side) {
if (side is MaterialStateBorderSide)
return MaterialStateProperty.resolveAs<BorderSide?>(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<Checkbox> 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)

View file

@ -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<RenderBox>(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<MaterialState> states) => side),
),
),
),
);
}
void expectBorder() {
expect(
tester.renderObject<RenderBox>(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 {