提交 08fce0e9 编写于 作者: M Matt Perry

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.
上级 834fb172
......@@ -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"
......
......@@ -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",
......
// 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<T> 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);
}
}
......@@ -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)
......
// 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));
}
......@@ -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();
}
......
......@@ -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<Point> {
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);
}
}
......@@ -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<Widget> 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
);
}
......
......@@ -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<BoxShadow> _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<BoxShadow> shadow = new List<BoxShadow>();
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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册