Skip to content

Commit 52a19e1

Browse files
committed
Support all tristate value transitions
1 parent fa20e3a commit 52a19e1

File tree

3 files changed

+120
-38
lines changed

3 files changed

+120
-38
lines changed

packages/flutter/lib/src/material/checkbox.dart

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ class Checkbox extends StatefulWidget {
7979
///
8080
/// When the checkbox is tapped, if [tristate] is false (the default) then
8181
/// the [onChanged] callback will be applied to `!value`. If [tristate] is
82-
/// true this callback will be applied to false if the current [value]
83-
/// is true, false otherwise.
82+
/// true this callback cycle from false to true to null.
8483
///
8584
/// The callback provided to [onChanged] should update the state of the parent
8685
/// [StatefulWidget] using the [State.setState] method, so that the parent
@@ -195,7 +194,7 @@ class _RenderCheckbox extends RenderToggleable {
195194
Color inactiveColor,
196195
ValueChanged<bool> onChanged,
197196
@required TickerProvider vsync,
198-
}): _showDash = value == null,
197+
}): _oldValue = value,
199198
super(
200199
value: value,
201200
tristate: tristate,
@@ -206,18 +205,54 @@ class _RenderCheckbox extends RenderToggleable {
206205
vsync: vsync,
207206
);
208207

209-
bool _showDash;
208+
bool _oldValue;
210209

211210
@override
212211
set value(bool newValue) {
213-
final bool oldValue = value;
214-
if (newValue == oldValue)
212+
if (newValue == value)
215213
return;
216-
_showDash = newValue == null || newValue == false && oldValue == null;
214+
_oldValue = value;
217215
super.value = newValue;
218216
}
219217

218+
// The square outer bounds of the checkbox at t, with the specified origin.
219+
// At t == 0.0, the outer rect's size is _kEdgeSize (Checkbox.width)
220+
// At t == 0.5, .. is _kEdgeSize - _kStrokeWidth
221+
// At t == 1.0, .. is _kEdgeSize
222+
RRect _outerRectAt(Offset origin, double t) {
223+
final double inset = 1.0 - (t - 0.5).abs() * 2.0;
224+
final double size = _kEdgeSize - inset * _kStrokeWidth;
225+
final Rect rect = new Rect.fromLTWH(origin.dx + inset, origin.dy + inset, size, size);
226+
return new RRect.fromRectAndRadius(rect, _kEdgeRadius);
227+
}
228+
229+
// The checkbox's border color if value == false, or its fill color when
230+
// value == true or null.
231+
Color _colorAt(double t) {
232+
// As t goes from 0.0 to 0.25, animate from the inactiveColor to activeColor.
233+
return onChanged == null
234+
? inactiveColor
235+
: (t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0));
236+
}
237+
238+
// White stroke used to paint the check and dash.
239+
void _initStrokePaint(Paint paint) {
240+
paint
241+
..color = const Color(0xFFFFFFFF)
242+
..style = PaintingStyle.stroke
243+
..strokeWidth = _kStrokeWidth;
244+
}
245+
246+
void _drawBorder(Canvas canvas, RRect outer, double t, Paint paint) {
247+
assert(t >= 0.0 && t <= 0.5);
248+
final double size = outer.width;
249+
// As t goes from 0.0 to 1.0, gradually fill the outer RRect.
250+
final RRect inner = outer.deflate(math.min(size / 2.0, _kStrokeWidth + size * t));
251+
canvas.drawDRRect(outer, inner, paint);
252+
}
253+
220254
void _drawCheck(Canvas canvas, Offset origin, double t, Paint paint) {
255+
assert(t >= 0.0 && t <= 1.0);
221256
// As t goes from 0.0 to 1.0, animate the two checkmark strokes from the
222257
// mid point outwards.
223258
final Path path = new Path();
@@ -233,6 +268,7 @@ class _RenderCheckbox extends RenderToggleable {
233268
}
234269

235270
void _drawDash(Canvas canvas, Offset origin, double t, Paint paint) {
271+
assert(t >= 0.0 && t <= 1.0);
236272
// As t goes from 0.0 to 1.0, animate the horizontal line from the
237273
// mid point outwards.
238274
const Offset start = const Offset(_kEdgeSize * 0.2, _kEdgeSize * 0.5);
@@ -249,33 +285,52 @@ class _RenderCheckbox extends RenderToggleable {
249285
paintRadialReaction(canvas, offset, size.center(Offset.zero));
250286

251287
final Offset origin = offset + (size / 2.0 - const Size.square(_kEdgeSize) / 2.0);
252-
final double t = position.value;
253-
final Color borderColor = (onChanged == null)
254-
? inactiveColor
255-
: (t >= 0.25 ? activeColor : Color.lerp(inactiveColor, activeColor, t * 4.0));
288+
final AnimationStatus status = position.status;
289+
final double tNormalized = status == AnimationStatus.forward || status == AnimationStatus.completed
290+
? position.value
291+
: 1.0 - position.value;
256292

257-
final double inset = 1.0 - (t - 0.5).abs() * 2.0;
258-
final double rectSize = _kEdgeSize - inset * _kStrokeWidth;
259-
final Rect rect = new Rect.fromLTWH(origin.dx + inset, origin.dy + inset, rectSize, rectSize);
260-
final RRect outer = new RRect.fromRectAndRadius(rect, _kEdgeRadius);
293+
// Four cases: false to null, false to true, null to false, true to false
294+
if (_oldValue == false || value == false) {
295+
final double t = value == false ? 1.0 - tNormalized : tNormalized;
296+
final RRect outer = _outerRectAt(origin, t);
297+
final Paint paint = new Paint()..color = _colorAt(t);
261298

262-
final Paint paint = new Paint()
263-
..color = borderColor;
299+
if (t <= 0.5) {
300+
_drawBorder(canvas, outer, t, paint);
301+
} else {
302+
canvas.drawRRect(outer, paint);
264303

265-
if (t <= 0.5) {
266-
final RRect inner = outer.deflate(math.min(rectSize / 2.0, _kStrokeWidth + rectSize * t));
267-
canvas.drawDRRect(outer, inner, paint);
268-
} else {
304+
_initStrokePaint(paint);
305+
final double tShrink = (t - 0.5) * 2.0;
306+
if (_oldValue == null)
307+
_drawDash(canvas, origin, tShrink, paint);
308+
else
309+
_drawCheck(canvas, origin, tShrink, paint);
310+
}
311+
} else { // Two cases: null to true, true to null
312+
final RRect outer = _outerRectAt(origin, 1.0);
313+
final Paint paint = new Paint() ..color = _colorAt(1.0);
269314
canvas.drawRRect(outer, paint);
270-
final double t = (position.value - 0.5) * 2.0;
271-
paint
272-
..color = const Color(0xFFFFFFFF)
273-
..style = PaintingStyle.stroke
274-
..strokeWidth = _kStrokeWidth;
275-
if (_showDash)
276-
_drawDash(canvas, origin, t, paint);
277-
else
278-
_drawCheck(canvas, origin, t, paint);
315+
316+
_initStrokePaint(paint);
317+
if (tNormalized <= 0.5) {
318+
final double tShrink = 1.0 - tNormalized * 2.0;
319+
if (_oldValue == true)
320+
_drawCheck(canvas, origin, tShrink, paint);
321+
else
322+
_drawDash(canvas, origin, tShrink, paint);
323+
} else {
324+
final double tExpand = (tNormalized - 0.5) * 2.0;
325+
if (value == true)
326+
_drawCheck(canvas, origin, tExpand, paint);
327+
else
328+
_drawDash(canvas, origin, tExpand, paint);
329+
}
279330
}
280331
}
332+
333+
// TODO(hmuller): smooth segues for cases where the value changes
334+
// in the middle of position's animation cycle.
335+
// https://github.com/flutter/flutter/issues/14674
281336
}

packages/flutter/lib/src/material/toggleable.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,14 @@ abstract class RenderToggleable extends RenderConstrainedBox {
134134
_position
135135
..curve = Curves.easeIn
136136
..reverseCurve = Curves.easeOut;
137-
if (value == false)
138-
_positionController.reverse();
139-
else
140-
_positionController.forward();
137+
switch (_positionController.status) {
138+
case AnimationStatus.forward:
139+
case AnimationStatus.completed:
140+
_positionController.reverse();
141+
break;
142+
default:
143+
_positionController.forward();
144+
}
141145
}
142146

143147
/// If true, [value] can be true, false, or null, otherwise [value] must
@@ -250,7 +254,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
250254
// the user dragged the toggleable: we may reach 0.0 or 1.0 without
251255
// seeing a tap. The Switch does this.
252256
void _handlePositionStateChanged(AnimationStatus status) {
253-
if (isInteractive) {
257+
if (isInteractive && !tristate) {
254258
if (status == AnimationStatus.completed && _value == false) {
255259
onChanged(true);
256260
}
@@ -268,8 +272,19 @@ abstract class RenderToggleable extends RenderConstrainedBox {
268272
}
269273

270274
void _handleTap() {
271-
if (isInteractive)
272-
onChanged(value == false);
275+
if (!isInteractive)
276+
return;
277+
switch (value) {
278+
case false:
279+
onChanged(true);
280+
break;
281+
case true:
282+
onChanged(tristate ? null : false);
283+
break;
284+
default: // case null:
285+
onChanged(false);
286+
break;
287+
}
273288
}
274289

275290
void _handleTapUp(TapUpDetails details) {

packages/flutter/test/material/checkbox_test.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ void main() {
103103
semantics.dispose();
104104
});
105105

106-
testWidgets('CheckBox tristate:true', (WidgetTester tester) async {
106+
testWidgets('CheckBox tristate: true', (WidgetTester tester) async {
107107
bool checkBoxValue;
108108

109109
await tester.pumpWidget(
@@ -133,5 +133,17 @@ void main() {
133133
await tester.tap(find.byType(Checkbox));
134134
await tester.pumpAndSettle();
135135
expect(checkBoxValue, true);
136+
137+
await tester.tap(find.byType(Checkbox));
138+
await tester.pumpAndSettle();
139+
expect(checkBoxValue, null);
140+
141+
checkBoxValue = true;
142+
await tester.pumpAndSettle();
143+
expect(checkBoxValue, true);
144+
145+
checkBoxValue = null;
146+
await tester.pumpAndSettle();
147+
expect(checkBoxValue, null);
136148
});
137149
}

0 commit comments

Comments
 (0)