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 6bbbbc8eb..cd959753c 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 bb2663b23..742be4b05 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 000000000..c730d8011 --- /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 60e3f90c6..48690407e 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 000000000..c40934c02 --- /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 6044b6448..4ce567f51 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 025e3a4fb..5b1451a88 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 c2c0fb7bf..5a2501a78 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 793fe9606..4120aa866 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; + + 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(ianh): we should make this animate level changes and color changes + // 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 -- GitLab