From 08fce0e9058668daebc6a912cf7a693f2ee01d9f Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 1 Jul 2015 16:38:20 -0400 Subject: [PATCH] Baby steps towards an odeon-like animation system. First victim: Drawer. This introduces an AnimationPerformance class, which is intended to manage an animation (or its reverse), with the ability to manually control the timeline or to apply a force to advance the animation with a diminishing speed. I'm having trouble fitting the odeon model to Sky. Odeon has a lot of nice properties, but fundamentally operates on UINodes, which contain all the properties to be animated. Sky, on the other hand, has no such universal properties. Instead, each Widget assembles itself how it sees fit. So my current plan is to let AnimationPerformance own a generic set of AnimatedVariables. You pass it a bag of things, say position and opacity, as AnimatedVariables. It updates them based on the animation, and they each have a way to build a widget based on their current state. R=abarth@chromium.org Review URL: https://codereview.chromium.org/1211603003. --- engine/core/painting/Offset.dart | 4 + sdk/BUILD.gn | 2 + sdk/lib/animation/animation_performance.dart | 89 ++++++++++++++++ sdk/lib/animation/generators.dart | 2 +- sdk/lib/base/lerp.dart | 19 ++++ sdk/lib/painting/box_painter.dart | 8 ++ sdk/lib/widgets/animated_component.dart | 23 ++++ sdk/lib/widgets/drawer.dart | 105 +++++++++---------- sdk/lib/widgets/material.dart | 51 +++++++-- 9 files changed, 235 insertions(+), 68 deletions(-) create mode 100644 sdk/lib/animation/animation_performance.dart create mode 100644 sdk/lib/base/lerp.dart diff --git a/engine/core/painting/Offset.dart b/engine/core/painting/Offset.dart index 6bbbbc8eb99..cd959753c14 100644 --- a/engine/core/painting/Offset.dart +++ b/engine/core/painting/Offset.dart @@ -21,6 +21,10 @@ class Offset extends OffsetBase { Offset operator -() => new Offset(-dx, -dy); Offset operator -(Offset other) => new Offset(dx - other.dx, dy - other.dy); Offset operator +(Offset other) => new Offset(dx + other.dx, dy + other.dy); + Offset operator *(double operand) => new Offset(dx * operand, dy * operand); + Offset operator /(double operand) => new Offset(dx / operand, dy / operand); + Offset operator ~/(double operand) => new Offset((dx ~/ operand).toDouble(), (dy ~/ operand).toDouble()); + Offset operator %(double operand) => new Offset(dx % operand, dy % operand); Rect operator &(Size other) => new Rect.fromLTWH(dx, dy, other.width, other.height); // does the equivalent of "return new Point(0,0) + this" diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index bb2663b23d5..742be4b053b 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -9,6 +9,7 @@ dart_pkg("sky") { "CHANGELOG.md", "bin/init.dart", "lib/animation/animated_value.dart", + "lib/animation/animation_performance.dart", "lib/animation/curves.dart", "lib/animation/fling_curve.dart", "lib/animation/generators.dart", @@ -18,6 +19,7 @@ dart_pkg("sky") { "lib/assets/material-design-icons.sha1", "lib/base/debug.dart", "lib/base/hit_test.dart", + "lib/base/lerp.dart", "lib/base/node.dart", "lib/base/scheduler.dart", "lib/download_material_design_icons", diff --git a/sdk/lib/animation/animation_performance.dart b/sdk/lib/animation/animation_performance.dart new file mode 100644 index 00000000000..c730d8011cb --- /dev/null +++ b/sdk/lib/animation/animation_performance.dart @@ -0,0 +1,89 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'animated_value.dart'; +import 'curves.dart'; + +// TODO(mpcomplete): merge this stuff with AnimatedValue somehow. We shouldn't +// have 2 different ways to animate values. +abstract class AnimatedVariable { + void setFraction(double t); +} + +class AnimatedType extends AnimatedVariable { + T value; + final T begin, end; + final Curve curve; + + AnimatedType(this.begin, this.end, {this.curve: linear}) { + value = begin; + } + + void setFraction(double t) { + // TODO(mpcomplete): Reverse the timeline and curve. + value = begin + (end - begin) * curve.transform(t); + } +} + +// This class manages a "performance" - a collection of values that change +// based on a timeline. For example, a performance may handle an animation +// of a menu opening by sliding and fading in (changing Y value and opacity) +// over .5 seconds. The performance can move forwards (present) or backwards +// (dismiss). A consumer may also take direct control of the timeline by +// manipulating |progress|, or |fling| the timeline causing a physics-based +// simulation to take over the progression. +class AnimationPerformance { + // TODO(mpcomplete): make this a list, or composable somehow. + AnimatedVariable variable; + // Advances from 0 to 1. On each tick, we'll update our variable's values. + AnimatedValue timeline = new AnimatedValue(0.0); + // TODO(mpcomplete): duration should be on a director. + Duration duration; + + AnimationPerformance() { + timeline.onValueChanged.listen((double t) { + variable.setFraction(t); + }); + } + + double get progress => timeline.value; + void set progress(double t) { + stop(); + timeline.value = t.clamp(0.0, 1.0); + } + + bool get isDismissed => progress == 0.0; + bool get isCompleted => progress == 1.0; + bool get isAnimating => timeline.isAnimating; + + void play() { + _animateTo(1.0); + } + void reverse() { + _animateTo(0.0); + } + + void _animateTo(double target) { + double remainingDistance = (target - timeline.value).abs(); + timeline.stop(); + if (remainingDistance != 0.0) + timeline.animateTo(target, remainingDistance * duration.inMilliseconds); + } + + void stop() { + timeline.stop(); + } + + // Resume animating in a direction, with the given velocity. + // TODO(mpcomplete): this should be a force with friction so it slows over + // time. + void fling({double velocity: 1.0}) { + double target = velocity.sign < 0.0 ? 0.0 : 1.0; + double distance = (target - timeline.value).abs(); + double duration = distance / velocity.abs(); + + if (distance > 0.0) + timeline.animateTo(target, duration, curve: linear); + } +} diff --git a/sdk/lib/animation/generators.dart b/sdk/lib/animation/generators.dart index 60e3f90c632..48690407e74 100644 --- a/sdk/lib/animation/generators.dart +++ b/sdk/lib/animation/generators.dart @@ -89,7 +89,7 @@ class AnimationGenerator extends Generator { startTime = timeStamp; double t = (timeStamp - (startTime + initialDelay)) / duration; - _lastTime = math.max(0.0, math.min(t, 1.0)); + _lastTime = t.clamp(0.0, 1.0); return _lastTime; }) .takeWhile(_checkForCompletion) diff --git a/sdk/lib/base/lerp.dart b/sdk/lib/base/lerp.dart new file mode 100644 index 00000000000..c40934c024b --- /dev/null +++ b/sdk/lib/base/lerp.dart @@ -0,0 +1,19 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:sky'; + +num lerpNum(num a, num b, double t) => a + (b - a) * t; + +Color lerpColor(Color a, Color b, double t) { + return new Color.fromARGB( + lerpNum(a.alpha, b.alpha, t).toInt(), + lerpNum(a.red, b.red, t).toInt(), + lerpNum(a.green, b.green, t).toInt(), + lerpNum(a.blue, b.blue, t).toInt()); +} + +Offset lerpOffset(Offset a, Offset b, double t) { + return new Offset(lerpNum(a.dx, b.dx, t), lerpNum(a.dy, b.dy, t)); +} diff --git a/sdk/lib/painting/box_painter.dart b/sdk/lib/painting/box_painter.dart index 6044b644844..4ce567f51fb 100644 --- a/sdk/lib/painting/box_painter.dart +++ b/sdk/lib/painting/box_painter.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'dart:sky' as sky; import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path; +import '../base/lerp.dart'; import 'shadows.dart'; import 'package:sky/mojo/net/image_cache.dart' as image_cache; @@ -72,6 +73,13 @@ class BoxShadow { String toString() => 'BoxShadow($color, $offset, $blur)'; } +BoxShadow lerpBoxShadow(BoxShadow a, BoxShadow b, double t) { + return new BoxShadow( + color: lerpColor(a.color, b.color, t), + offset: lerpOffset(a.offset, b.offset, t), + blur: lerpNum(a.blur, b.blur, t)); +} + abstract class Gradient { sky.Shader createShader(); } diff --git a/sdk/lib/widgets/animated_component.dart b/sdk/lib/widgets/animated_component.dart index 025e3a4fb3f..5b1451a8873 100644 --- a/sdk/lib/widgets/animated_component.dart +++ b/sdk/lib/widgets/animated_component.dart @@ -4,7 +4,11 @@ import 'dart:async'; +import 'package:vector_math/vector_math.dart'; + import '../animation/animated_value.dart'; +import '../animation/animation_performance.dart'; +import '../animation/curves.dart'; import 'basic.dart'; class _AnimationEntry { @@ -50,3 +54,22 @@ abstract class AnimatedComponent extends Component { } } + +// Types of things that can be animated in a component. Use build() to +// construct the final Widget based on the animation state. +// TODO(mpcomplete): the idea here is to eventually have an AnimatedCollection +// which assembles a container based on a list of animated things. e.g. if you +// want to animate position, opacity, and shadow, you add those animators to an +// AnimatedCollection and just call collection.build() to construct your +// widget. + +class AnimatedPosition extends AnimatedType { + AnimatedPosition(Point begin, Point end, {Curve curve: linear}) + : super(begin, end, curve: curve); + + Widget build(Widget child) { + Matrix4 transform = new Matrix4.identity(); + transform.translate(value.x, value.y); + return new Transform(transform: transform, child: child); + } +} diff --git a/sdk/lib/widgets/drawer.dart b/sdk/lib/widgets/drawer.dart index c2c0fb7bfcb..5a2501a7850 100644 --- a/sdk/lib/widgets/drawer.dart +++ b/sdk/lib/widgets/drawer.dart @@ -8,6 +8,7 @@ import 'dart:sky' as sky; import 'package:vector_math/vector_math.dart'; import '../animation/animated_value.dart'; +import '../animation/animation_performance.dart'; import '../animation/curves.dart'; import '../theme/colors.dart'; import '../theme/shadows.dart'; @@ -29,22 +30,26 @@ import 'basic.dart'; const double _kWidth = 304.0; const double _kMinFlingVelocity = 0.4; -const double _kBaseSettleDurationMS = 246.0; -const double _kMaxSettleDurationMS = 600.0; +const int _kBaseSettleDurationMS = 246; const Curve _kAnimationCurve = parabolicRise; typedef void DrawerStatusChangeHandler (bool showing); class DrawerController { - DrawerController(this.onStatusChange) { - position = new AnimatedValue(-_kWidth, onChange: _checkValue); + performance = new AnimationPerformance() + ..duration = new Duration(milliseconds: _kBaseSettleDurationMS) + ..variable = position; + performance.timeline.onValueChanged.listen(_checkValue); } final DrawerStatusChangeHandler onStatusChange; - AnimatedValue position; + + AnimationPerformance performance; + final AnimatedPosition position = new AnimatedPosition( + new Point(-_kWidth, 0.0), Point.origin, curve: _kAnimationCurve); bool _oldClosedState = true; - void _checkValue() { + void _checkValue(_) { var newClosedState = isClosed; if (onStatusChange != null && _oldClosedState != newClosedState) { onStatusChange(!newClosedState); @@ -52,69 +57,52 @@ class DrawerController { } } - bool get isClosed => position.value == -_kWidth; - bool get _isMostlyClosed => position.value <= -_kWidth / 2; - void toggle() => _isMostlyClosed ? open() : close(); + bool get isClosed => performance.isDismissed; + bool get _isMostlyClosed => position.value.x <= -_kWidth/2; + + void open() => performance.play(); + + void close() => performance.reverse(); + + void _settle() => _isMostlyClosed ? close() : open(); void handleMaskTap(_) => close(); - void handlePointerDown(_) => position.stop(); + + // TODO(mpcomplete): Figure out how to generalize these handlers on a + // "PannableThingy" interface. + void handlePointerDown(_) => performance.stop(); void handlePointerMove(sky.PointerEvent event) { - if (position.isAnimating) + if (performance.isAnimating) return; - position.value = math.min(0.0, math.max(position.value + event.dx, -_kWidth)); + performance.progress += event.dx / _kWidth; } void handlePointerUp(_) { - if (!position.isAnimating) + if (!performance.isAnimating) _settle(); } void handlePointerCancel(_) { - if (!position.isAnimating) + if (!performance.isAnimating) _settle(); } - void open() => _animateToPosition(0.0); - - void close() => _animateToPosition(-_kWidth); - - void _settle() => _isMostlyClosed ? close() : open(); - - void _animateToPosition(double targetPosition) { - double distance = (targetPosition - position.value).abs(); - if (distance != 0) { - double targetDuration = distance / _kWidth * _kBaseSettleDurationMS; - double duration = math.min(targetDuration, _kMaxSettleDurationMS); - position.animateTo(targetPosition, duration, curve: _kAnimationCurve); - } - } - void handleFlingStart(event) { - double direction = event.velocityX.sign; - double velocityX = event.velocityX.abs() / 1000; - if (velocityX < _kMinFlingVelocity) - return; - - double targetPosition = direction < 0.0 ? -_kWidth : 0.0; - double distance = (targetPosition - position.value).abs(); - double duration = distance / velocityX; - - if (distance > 0) - position.animateTo(targetPosition, duration, curve: linear); + double velocityX = event.velocityX / 1000; + if (velocityX.abs() >= _kMinFlingVelocity) + performance.fling(velocity: velocityX / _kWidth); } - } class Drawer extends AnimatedComponent { - Drawer({ String key, this.controller, this.children, this.level: 0 }) : super(key: key) { - watch(controller.position); + watch(controller.performance.timeline); } List children; @@ -128,34 +116,35 @@ class Drawer extends AnimatedComponent { super.syncFields(source); } + // TODO(mpcomplete): the animation system should handle building, maybe? Or + // at least setting the transform. Figure out how this could work for things + // like fades, slides, rotates, pinch, etc. Widget build() { - Matrix4 transform = new Matrix4.identity(); - transform.translate(controller.position.value); - - double scaler = controller.position.value / _kWidth + 1; + // TODO(mpcomplete): animate as a fade-in. + double scaler = controller.performance.progress + 1.0; Color maskColor = new Color.fromARGB((0x7F * scaler).floor(), 0, 0, 0); var mask = new Listener( child: new Container(decoration: new BoxDecoration(backgroundColor: maskColor)), - onGestureTap: controller.handleMaskTap, - onGestureFlingStart: controller.handleFlingStart + onGestureTap: controller.handleMaskTap ); - Container content = new Container( - decoration: new BoxDecoration( - backgroundColor: Grey[50], - boxShadow: shadows[level]), - width: _kWidth, - transform: transform, - child: new Block(children) - ); + Widget content = controller.position.build( + new Container( + decoration: new BoxDecoration( + backgroundColor: Grey[50], + boxShadow: shadows[level]), + width: _kWidth, + child: new Block(children) + )); return new Listener( child: new Stack([ mask, content ]), onPointerDown: controller.handlePointerDown, onPointerMove: controller.handlePointerMove, onPointerUp: controller.handlePointerUp, - onPointerCancel: controller.handlePointerCancel + onPointerCancel: controller.handlePointerCancel, + onGestureFlingStart: controller.handleFlingStart ); } diff --git a/sdk/lib/widgets/material.dart b/sdk/lib/widgets/material.dart index 793fe960663..4120aa8665c 100644 --- a/sdk/lib/widgets/material.dart +++ b/sdk/lib/widgets/material.dart @@ -2,36 +2,69 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import '../base/lerp.dart'; +import '../animation/animated_value.dart'; import '../painting/box_painter.dart'; import '../theme/edges.dart'; import '../theme/shadows.dart'; +import 'animated_component.dart'; import 'basic.dart'; import 'default_text_style.dart'; import 'theme.dart'; export '../theme/edges.dart' show MaterialEdge; -class Material extends Component { +const double _kAnimateShadowDurationMS = 100.0; + +List _computeShadow(double level) { + if (level < 1.0) // shadows[1] is the first shadow + return null; + + int level1 = level.floor(); + int level2 = level.ceil(); + double t = level - level1.toDouble(); + + List shadow = new List(); + for (int i = 0; i < shadows[level1].length; ++i) + shadow.add(lerpBoxShadow(shadows[level1][i], shadows[level2][i], t)); + return shadow; +} + +class Material extends AnimatedComponent { Material({ String key, this.child, this.edge: MaterialEdge.card, - this.level: 0, + int level: 0, this.color - }) : super(key: key); + }) : super(key: key) { + this.level = new AnimatedValue(level.toDouble()); + watch(this.level); + } - final Widget child; - final int level; - final MaterialEdge edge; - final Color color; + Widget child; + MaterialEdge edge; + AnimatedValue level; + Color color; - // TODO(ianh): we should make this animate level changes and color changes + void syncFields(Material source) { + child = source.child; + edge = source.edge; + // TODO(mpcomplete): duration is wrong, because the current level may be + // anything. We really want |rate|. + if (level.value != source.level.value) + level.animateTo(source.level.value.toDouble(), _kAnimateShadowDurationMS); + color = source.color; + super.syncFields(source); + } + + // TODO(mpcomplete): make this animate color changes. Widget build() { return new Container( decoration: new BoxDecoration( - boxShadow: shadows[level], + boxShadow: _computeShadow(level.value), borderRadius: edges[edge], backgroundColor: color, shape: edge == MaterialEdge.circle ? Shape.circle : Shape.rectangle