From dd1df9f5edf4fbea39ce439c59cb33cd18c30a0f Mon Sep 17 00:00:00 2001 From: Collin Jackson Date: Fri, 12 Jun 2015 12:45:11 -0700 Subject: [PATCH] Animate checkboxes. I based it on the dialer app. Could probably use more improvement, but I think this is an improvement on what we are doing currently. R=abarth@chromium.org, abarth Review URL: https://codereview.chromium.org/1182743002. --- sdk/lib/framework/widgets/checkbox.dart | 109 +++++++++++++++++++----- sdk/lib/framework/widgets/wrappers.dart | 6 +- 2 files changed, 90 insertions(+), 25 deletions(-) diff --git a/sdk/lib/framework/widgets/checkbox.dart b/sdk/lib/framework/widgets/checkbox.dart index 1d2630c4848..a6b34184489 100644 --- a/sdk/lib/framework/widgets/checkbox.dart +++ b/sdk/lib/framework/widgets/checkbox.dart @@ -6,22 +6,54 @@ import 'dart:sky' as sky; import 'package:sky/framework/theme2/colors.dart' as colors; +import '../animation/animated_value.dart'; +import '../animation/curves.dart'; import '../rendering/box.dart'; -import 'button_base.dart'; +import 'animated_component.dart'; import 'wrappers.dart'; typedef void ValueChanged(value); -class Checkbox extends ButtonBase { +const double _kMidpoint = 0.5; +const double _kCheckDuration = 200.0; +const sky.Color _kUncheckedColor = const sky.Color(0x8A000000); +// TODO(jackson): This should change colors with the theme +sky.Color _kCheckedColor = colors.Purple[500]; +const double _kEdgeSize = 20.0; +const double _kEdgeRadius = 1.0; - Checkbox({ Object key, this.onChanged, this.checked }) : super(key: key); +class Checkbox extends AnimatedComponent { + + Checkbox({ + Object key, + this.checked, + this.onChanged + }) : super(key: key) { + _checkedAnimation = new AnimatedValue(checked ? 1.0 : 0.0); + } bool checked; + AnimatedValue _checkedAnimation; ValueChanged onChanged; void syncFields(Checkbox source) { - checked = source.checked; onChanged = source.onChanged; + if (checked != source.checked) { + checked = source.checked; + double targetValue = checked ? 1.0 : 0.0; + double difference = (_checkedAnimation.value - targetValue).abs(); + if (difference > 0) { + double duration = difference * _kCheckDuration; + _checkedAnimation.stop(); + Curve curve; + if (targetValue > _checkedAnimation.value) { + curve = easeIn; + } else { + curve = easeOut; + } + _checkedAnimation.animateTo(targetValue, duration, curve: curve); + } + } super.syncFields(source); } @@ -29,37 +61,68 @@ class Checkbox extends ButtonBase { onChanged(!checked); } - UINode buildContent() { - // TODO(jackson): This should change colors with the theme - sky.Color color = highlight ? colors.Purple[500] : const sky.Color(0x8A000000); - const double kEdgeSize = 20.0; - const double kEdgeRadius = 1.0; + UINode build() { return new EventListenerNode( new Container( margin: const EdgeDims.symmetric(horizontal: 5.0), - width: kEdgeSize + 2.0, - height: kEdgeSize + 2.0, + width: _kEdgeSize + 2.0, + height: _kEdgeSize + 2.0, child: new CustomPaint( + token: _checkedAnimation.value, callback: (sky.Canvas canvas, Size size) { + // Choose a color between grey and the theme color + sky.Paint paint = new sky.Paint()..strokeWidth = 2.0 + ..color = _kUncheckedColor; - sky.Paint paint = new sky.Paint()..color = color - ..strokeWidth = 2.0; + // The rrect contracts slightly during the animation + double inset = 2.0 - (_checkedAnimation.value - _kMidpoint).abs() * 2.0; + sky.Rect rect = new sky.Rect.fromLTRB(inset, inset, _kEdgeSize - inset, _kEdgeSize - inset); + sky.RRect rrect = new sky.RRect()..setRectXY(rect, _kEdgeRadius, _kEdgeRadius); - // Draw the outer rrect - paint.setStyle(checked ? sky.PaintingStyle.strokeAndFill : sky.PaintingStyle.stroke); - sky.Rect rect = new sky.Rect.fromLTRB(0.0, 0.0, kEdgeSize, kEdgeSize); - sky.RRect rrect = new sky.RRect()..setRectXY(rect, kEdgeRadius, kEdgeRadius); + + // Outline of the empty rrect + paint.setStyle(sky.PaintingStyle.stroke); canvas.drawRRect(rrect, paint); - // Draw the inner check - if (checked) { - // TODO(jackson): Use the theme color + // Radial gradient that changes size + if (_checkedAnimation.value > 0) { + paint.setStyle(sky.PaintingStyle.fill); + paint.setShader( + new sky.Gradient.radial( + new Point(_kEdgeSize / 2.0, _kEdgeSize / 2.0), + _kEdgeSize * (_kMidpoint - _checkedAnimation.value) * 8.0, + [const sky.Color(0x00000000), _kUncheckedColor], + [0.0, 1.0] + ) + ); + canvas.drawRRect(rrect, paint); + } + + if (_checkedAnimation.value > _kMidpoint) { + double t = (_checkedAnimation.value - _kMidpoint) / (1.0 - _kMidpoint); + + // Solid filled rrect + paint.setStyle(sky.PaintingStyle.strokeAndFill); + paint.color = new Color.fromARGB((t * 255).floor(), + _kCheckedColor.red, + _kCheckedColor.green, + _kCheckedColor.blue); + canvas.drawRRect(rrect, paint); + + // White inner check paint.color = const sky.Color(0xFFFFFFFF); paint.setStyle(sky.PaintingStyle.stroke); sky.Path path = new sky.Path(); - path.moveTo(kEdgeSize * 0.2, kEdgeSize * 0.5); - path.lineTo(kEdgeSize * 0.4, kEdgeSize * 0.7); - path.lineTo(kEdgeSize * 0.8, kEdgeSize * 0.3); + sky.Point start = new sky.Point(_kEdgeSize * 0.2, _kEdgeSize * 0.5); + sky.Point mid = new sky.Point(_kEdgeSize * 0.4, _kEdgeSize * 0.7); + sky.Point end = new sky.Point(_kEdgeSize * 0.8, _kEdgeSize * 0.3); + Point lerp(Point p1, Point p2, double t) + => new Point(p1.x * (1.0 - t) + p2.x * t, p1.y * (1.0 - t) + p2.y * t); + sky.Point drawStart = lerp(start, mid, 1.0 - t); + sky.Point drawEnd = lerp(mid, end, t); + path.moveTo(drawStart.x, drawStart.y); + path.lineTo(mid.x, mid.y); + path.lineTo(drawEnd.x, drawEnd.y); canvas.drawPath(path, paint); } } diff --git a/sdk/lib/framework/widgets/wrappers.dart b/sdk/lib/framework/widgets/wrappers.dart index 63620544a54..676afa65fa7 100644 --- a/sdk/lib/framework/widgets/wrappers.dart +++ b/sdk/lib/framework/widgets/wrappers.dart @@ -49,19 +49,21 @@ class DecoratedBox extends OneChildRenderObjectWrapper { } -// TODO(jackson) need a mechanism for marking the RenderCustomPaint as needing paint class CustomPaint extends OneChildRenderObjectWrapper { - CustomPaint({ this.callback, UINode child, Object key }) + CustomPaint({ this.callback, this.token, UINode child, Object key }) : super(child: child, key: key); RenderCustomPaint get root { RenderCustomPaint result = super.root; return result; } final CustomPaintCallback callback; + final dynamic token; // set this to be repainted automatically when the token changes RenderCustomPaint createNode() => new RenderCustomPaint(callback: callback); void syncRenderObject(CustomPaint old) { super.syncRenderObject(old); + if (old != null && old.token != token) + root.markNeedsPaint(); root.callback = callback; }