Unverified Commit 21b27088 authored by Yash Johri's avatar Yash Johri Committed by GitHub

[Checkbox] Adds shape property (#70171)

parent 8104c578
......@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -71,6 +72,8 @@ class Checkbox extends StatefulWidget {
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.shape,
this.side,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(autofocus != null),
......@@ -263,6 +266,23 @@ class Checkbox extends StatefulWidget {
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@template flutter.material.checkbox.shape}
/// The shape of the checkbox's [Material].
/// {@endtemplate}
///
/// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
/// is used. If that's null then the shape will be a [RoundedRectangleBorder]
/// with a circular corner radius of 1.0.
final OutlinedBorder? shape;
/// {@template flutter.material.checkbox.side}
/// The side of the checkbox's border.
/// {@endtemplate}
///
/// If this property is null then [CheckboxThemeData.side] of [ThemeData.checkboxTheme]
/// is used. If that's null then the side will be width 2.
final BorderSide? side;
/// The width of a checkbox widget.
static const double width = 18.0;
......@@ -435,6 +455,10 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
vsync: this,
hasFocus: _focused,
hovering: _hovering,
side: widget.side ?? themeData.checkboxTheme.side,
shape: widget.shape ?? themeData.checkboxTheme.shape ?? const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(1.0)),
),
);
},
),
......@@ -460,6 +484,8 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
required this.additionalConstraints,
required this.hasFocus,
required this.hovering,
required this.shape,
required this.side,
}) : assert(tristate != null),
assert(tristate || value != null),
assert(activeColor != null),
......@@ -482,6 +508,8 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
final ValueChanged<bool?>? onChanged;
final TickerProvider vsync;
final BoxConstraints additionalConstraints;
final OutlinedBorder shape;
final BorderSide? side;
@override
_RenderCheckbox createRenderObject(BuildContext context) => _RenderCheckbox(
......@@ -500,6 +528,8 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
additionalConstraints: additionalConstraints,
hasFocus: hasFocus,
hovering: hovering,
shape: shape,
side: side,
);
@override
......@@ -521,12 +551,13 @@ class _CheckboxRenderObjectWidget extends LeafRenderObjectWidget {
..additionalConstraints = additionalConstraints
..vsync = vsync
..hasFocus = hasFocus
..hovering = hovering;
..hovering = hovering
..shape = shape
..side = side;
}
}
const double _kEdgeSize = Checkbox.width;
const Radius _kEdgeRadius = Radius.circular(1.0);
const double _kStrokeWidth = 2.0;
class _RenderCheckbox extends RenderToggleable {
......@@ -545,6 +576,8 @@ class _RenderCheckbox extends RenderToggleable {
ValueChanged<bool?>? onChanged,
required bool hasFocus,
required bool hovering,
required this.shape,
required this.side,
required TickerProvider vsync,
}) : _oldValue = value,
super(
......@@ -566,6 +599,8 @@ class _RenderCheckbox extends RenderToggleable {
bool? _oldValue;
Color checkColor;
OutlinedBorder shape;
BorderSide? side;
@override
set value(bool? newValue) {
......@@ -585,11 +620,11 @@ class _RenderCheckbox extends RenderToggleable {
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
// At t == 1.0, .. is _kEdgeSize
RRect _outerRectAt(Offset origin, double t) {
Rect _outerRectAt(Offset origin, double t) {
final double inset = 1.0 - (t - 0.5).abs() * 2.0;
final double size = _kEdgeSize - inset * _kStrokeWidth;
final Rect rect = Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size);
return RRect.fromRectAndRadius(rect, _kEdgeRadius);
return rect;
}
// The checkbox's border color if value == false, or its fill color when
......@@ -607,12 +642,12 @@ class _RenderCheckbox extends RenderToggleable {
..strokeWidth = _kStrokeWidth;
}
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
void _drawBorder(Canvas canvas, Rect outer, double t, Paint paint) {
assert(t >= 0.0 && t <= 0.5);
final double size = outer.width;
// As t goes from 0.0 to 1.0, gradually fill the outer RRect.
final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
canvas.drawDRRect(outer, inner, paint);
if (side == null) {
shape = shape.copyWith(side: BorderSide(width: 2, color: paint.color));
}
shape.copyWith(side: side).paint(canvas, outer);
}
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
......@@ -665,13 +700,14 @@ class _RenderCheckbox extends RenderToggleable {
// Four cases: false to null, false to true, null to false, true to false
if (_oldValue == false || value == false) {
final double t = value == false ? 1.0 - tNormalized : tNormalized;
final RRect outer = _outerRectAt(origin, t);
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);
} else {
canvas.drawRRect(outer, paint);
canvas.drawPath(emptyCheckboxPath, paint);
final double tShrink = (t - 0.5) * 2.0;
if (_oldValue == null || value == null)
......@@ -680,9 +716,9 @@ class _RenderCheckbox extends RenderToggleable {
_drawCheck(canvas, origin, tShrink, strokePaint);
}
} else { // Two cases: null to true, true to null
final RRect outer = _outerRectAt(origin, 1.0);
final Rect outer = _outerRectAt(origin, 1.0);
final Paint paint = Paint() ..color = _colorAt(1.0);
canvas.drawRRect(outer, paint);
canvas.drawPath(shape.copyWith(side: side).getOuterPath(outer), paint);
if (tNormalized <= 0.5) {
final double tShrink = 1.0 - tNormalized * 2.0;
......
......@@ -41,6 +41,8 @@ class CheckboxThemeData with Diagnosticable {
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.shape,
this.side,
});
/// {@macro flutter.material.checkbox.mouseCursor}
......@@ -85,6 +87,16 @@ class CheckboxThemeData with Diagnosticable {
/// If specified, overrides the default value of [Checkbox.visualDensity].
final VisualDensity? visualDensity;
/// {@macro flutter.material.checkbox.shape}
///
/// If specified, overrides the default value of [Checkbox.shape].
final OutlinedBorder? shape;
/// {@macro flutter.material.checkbox.side}
///
/// If specified, overrides the default value of [Checkbox.side].
final BorderSide? side;
/// Creates a copy of this object but with the given fields replaced with the
/// new values.
CheckboxThemeData copyWith({
......@@ -95,6 +107,8 @@ class CheckboxThemeData with Diagnosticable {
double? splashRadius,
MaterialTapTargetSize? materialTapTargetSize,
VisualDensity? visualDensity,
OutlinedBorder? shape,
BorderSide? side,
}) {
return CheckboxThemeData(
mouseCursor: mouseCursor ?? this.mouseCursor,
......@@ -104,6 +118,8 @@ class CheckboxThemeData with Diagnosticable {
splashRadius: splashRadius ?? this.splashRadius,
materialTapTargetSize: materialTapTargetSize ?? this.materialTapTargetSize,
visualDensity: visualDensity ?? this.visualDensity,
shape: shape ?? this.shape,
side: side ?? this.side,
);
}
......@@ -119,6 +135,8 @@ class CheckboxThemeData with Diagnosticable {
splashRadius: lerpDouble(a?.splashRadius, b?.splashRadius, t),
materialTapTargetSize: t < 0.5 ? a?.materialTapTargetSize : b?.materialTapTargetSize,
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
shape: ShapeBorder.lerp(a?.shape, b?.shape, t) as OutlinedBorder?,
side: _lerpSides(a?.side, b?.side, t),
);
}
......@@ -132,6 +150,8 @@ class CheckboxThemeData with Diagnosticable {
splashRadius,
materialTapTargetSize,
visualDensity,
shape,
side,
);
}
......@@ -148,7 +168,9 @@ class CheckboxThemeData with Diagnosticable {
&& other.overlayColor == overlayColor
&& other.splashRadius == splashRadius
&& other.materialTapTargetSize == materialTapTargetSize
&& other.visualDensity == visualDensity;
&& other.visualDensity == visualDensity
&& other.shape == shape
&& other.side == side;
}
@override
......@@ -161,6 +183,8 @@ class CheckboxThemeData with Diagnosticable {
properties.add(DoubleProperty('splashRadius', splashRadius, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialTapTargetSize>('materialTapTargetSize', materialTapTargetSize, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
properties.add(DiagnosticsProperty<OutlinedBorder>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<BorderSide>('side', side, defaultValue: null));
}
static MaterialStateProperty<T>? _lerpProperties<T>(
......@@ -174,6 +198,13 @@ class CheckboxThemeData with Diagnosticable {
return null;
return _LerpProperties<T>(a, b, t, lerpFunction);
}
// Special case because BorderSide.lerp() doesn't support null arguments
static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) {
if (a == null && b == null)
return null;
return BorderSide.lerp(a!, b!, t);
}
}
class _LerpProperties<T> implements MaterialStateProperty<T> {
......
......@@ -34,6 +34,9 @@ void main() {
});
testWidgets('CheckboxListTile checkColor test', (WidgetTester tester) async {
const Color checkBoxBorderColor = Color(0xff1e88e5);
Color checkBoxCheckColor = const Color(0xffFFFFFF);
Widget buildFrame(Color? color) {
return wrap(
child: CheckboxListTile(
......@@ -50,11 +53,13 @@ void main() {
await tester.pumpWidget(buildFrame(null));
await tester.pumpAndSettle();
expect(getCheckboxListTileRenderer(), paints..path(color: const Color(0xFFFFFFFF)));
expect(getCheckboxListTileRenderer(), paints..path(color: checkBoxBorderColor)..path(color: checkBoxCheckColor));
checkBoxCheckColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(const Color(0xFF000000)));
await tester.pumpWidget(buildFrame(checkBoxCheckColor));
await tester.pumpAndSettle();
expect(getCheckboxListTileRenderer(), paints..path(color: const Color(0xFF000000)));
expect(getCheckboxListTileRenderer(), paints..path(color: checkBoxBorderColor)..path(color: checkBoxCheckColor));
});
testWidgets('CheckboxListTile activeColor test', (WidgetTester tester) async {
......@@ -76,11 +81,11 @@ void main() {
await tester.pumpWidget(buildFrame(const Color(0xFF000000), null));
await tester.pumpAndSettle();
expect(getCheckboxListTileRenderer(), paints..rrect(color: const Color(0xFF000000)));
expect(getCheckboxListTileRenderer(), paints..path(color: const Color(0xFF000000)));
await tester.pumpWidget(buildFrame(const Color(0xFF000000), const Color(0xFFFFFFFF)));
await tester.pumpAndSettle();
expect(getCheckboxListTileRenderer(), paints..rrect(color: const Color(0xFFFFFFFF)));
expect(getCheckboxListTileRenderer(), paints..path(color: const Color(0xFFFFFFFF)));
});
testWidgets('CheckboxListTile can autofocus unless disabled.', (WidgetTester tester) async {
......
......@@ -355,6 +355,10 @@ void main() {
});
testWidgets('CheckBox color rendering', (WidgetTester tester) async {
const Color borderColor = Color(0xff1e88e5);
Color checkColor = const Color(0xffFFFFFF);
Color activeColor;
Widget buildFrame({Color? activeColor, Color? checkColor, ThemeData? themeData}) {
return Material(
child: Theme(
......@@ -379,21 +383,27 @@ void main() {
}));
}
await tester.pumpWidget(buildFrame(checkColor: const Color(0xFFFFFFFF)));
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: const Color(0xFFFFFFFF))); // paints's color is 0xFFFFFFFF (default color)
expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFFFFFFFF (default color)
checkColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(checkColor: const Color(0xFF000000)));
await tester.pumpWidget(buildFrame(checkColor: checkColor));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: const Color(0xFF000000))); // paints's color is 0xFF000000 (params)
expect(getCheckboxRenderer(), paints..path(color: borderColor)..path(color: checkColor)); // paints's color is 0xFF000000 (params)
activeColor = const Color(0xFF00FF00);
await tester.pumpWidget(buildFrame(themeData: ThemeData(toggleableActiveColor: const Color(0xFF00FF00))));
await tester.pumpWidget(buildFrame(themeData: ThemeData(toggleableActiveColor: activeColor)));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: const Color(0xFF00FF00))); // paints's color is 0xFF00FF00 (theme)
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // paints's color is 0xFF00FF00 (theme)
await tester.pumpWidget(buildFrame(activeColor: const Color(0xFF000000)));
activeColor = const Color(0xFF000000);
await tester.pumpWidget(buildFrame(activeColor: activeColor));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: const Color(0xFF000000))); // paints's color is 0xFF000000 (params)
expect(getCheckboxRenderer(), paints..path(color: activeColor)); // paints's color is 0xFF000000 (params)
});
testWidgets('Checkbox is focusable and has correct focus color', (WidgetTester tester) async {
......@@ -429,10 +439,7 @@ void main() {
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..rrect(
color: const Color(0xff1e88e5),
rrect: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
..path(color: const Color(0xff1e88e5))
..path(color: Colors.white),
);
......@@ -525,11 +532,8 @@ void main() {
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..rrect(
color: const Color(0xff1e88e5),
rrect: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
..path(color: const Color(0xff1e88e5))
..path(color: const Color(0xffffffff),style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Start hovering
......@@ -542,10 +546,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..rrect(
color: const Color(0xff1e88e5),
rrect: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
..path(color: const Color(0xff1e88e5))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
);
......@@ -555,10 +556,7 @@ void main() {
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..rrect(
color: const Color(0x61000000),
rrect: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(1.0)))
..path(color: const Color(0x61000000))
..path(color: const Color(0xffffffff), style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
......@@ -835,11 +833,11 @@ void main() {
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: activeEnabledFillColor));
expect(getCheckboxRenderer(), paints..path(color: activeEnabledFillColor));
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: activeDisabledFillColor));
expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor));
});
testWidgets('Checkbox fill color resolves in hovered/focused states', (WidgetTester tester) async {
......@@ -889,7 +887,7 @@ void main() {
await tester.pumpWidget(buildFrame());
await tester.pumpAndSettle();
expect(focusNode.hasPrimaryFocus, isTrue);
expect(getCheckboxRenderer(), paints..rrect(color: focusedFillColor));
expect(getCheckboxRenderer(), paints..path(color: focusedFillColor));
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
......@@ -898,7 +896,53 @@ void main() {
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..rrect(color: hoveredFillColor));
expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor));
});
testWidgets('Checkbox respects shape and side', (WidgetTester tester) async {
final RoundedRectangleBorder roundedRectangleBorder =
RoundedRectangleBorder(borderRadius: BorderRadius.circular(5));
const BorderSide side = BorderSide(
width: 4,
color: Color(0xfff44336),
);
Widget buildApp() {
return MaterialApp(
home: Material(
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Checkbox(
value: false,
onChanged: (bool? newValue) {},
shape: roundedRectangleBorder,
side: side,
);
}),
),
),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(tester.widget<Checkbox>(find.byType(Checkbox)).shape,
roundedRectangleBorder);
expect(tester.widget<Checkbox>(find.byType(Checkbox)).side, side);
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..drrect(
color: const Color(0xfff44336),
outer: RRect.fromLTRBR(
391.0, 291.0, 409.0, 309.0, const Radius.circular(5)),
inner: RRect.fromLTRBR(
395.0, 295.0, 405.0, 305.0, const Radius.circular(1)))
,
);
});
testWidgets('Checkbox overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async {
......
......@@ -138,8 +138,8 @@ void main() {
// Selected checkbox.
await tester.pumpWidget(buildCheckbox(selected: true));
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..rrect(color: selectedFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: defaultCheckColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor)..path(color: defaultCheckColor));
// Checkbox with hover.
await tester.pumpWidget(buildCheckbox());
......@@ -152,7 +152,7 @@ void main() {
await tester.pumpWidget(buildCheckbox(autofocus: true, selected: true));
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..circle(color: focusOverlayColor, radius: splashRadius));
expect(_getCheckboxMaterial(tester), paints..path(color: focusedCheckColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor)..path(color: focusedCheckColor));
});
testWidgets('Checkbox properties are taken over the theme values', (WidgetTester tester) async {
......@@ -237,8 +237,8 @@ void main() {
// Selected checkbox.
await tester.pumpWidget(buildCheckbox(selected: true));
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..rrect(color: selectedFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: checkColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor)..path(color: checkColor));
// Checkbox with hover.
await tester.pumpWidget(buildCheckbox());
......@@ -288,7 +288,7 @@ void main() {
// Selected checkbox.
await tester.pumpWidget(buildCheckbox(selected: true));
await tester.pumpAndSettle();
expect(_getCheckboxMaterial(tester), paints..rrect(color: selectedFillColor));
expect(_getCheckboxMaterial(tester), paints..path(color: selectedFillColor));
});
testWidgets('Checkbox theme overlay color resolves in active/pressed states', (WidgetTester tester) async {
......
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