Commit bc20871c authored by Adam Barth's avatar Adam Barth

Introduce Tween and the new animation API

This patch removes state from the animation system, which was causing problems
as we were scaling the use of animated values.

Now the "tween" objects are stateless and can watch animations, which creates a
new object that holds both the tween and the animation instead of mutating the
tween every tick of the animation.

This patch ports one client as a proof-of-concept.

Fixes #215
parent 548dbf08
......@@ -12,39 +12,38 @@ class ProgressIndicatorApp extends StatefulComponent {
class _ProgressIndicatorAppState extends State<ProgressIndicatorApp> {
void initState() {
super.initState();
valueAnimation = new ValuePerformance<double>()
..duration = const Duration(milliseconds: 1500)
..variable = new AnimatedValue<double>(
0.0,
end: 1.0,
curve: new Interval(0.0, 0.9, curve: Curves.ease),
reverseCurve: Curves.ease
);
valueAnimation.addStatusListener((PerformanceStatus status) {
controller = new AnimationController(
duration: const Duration(milliseconds: 1500)
)..play(AnimationDirection.forward);
animation = new ACurve(
parent: controller,
curve: new Interval(0.0, 0.9, curve: Curves.ease),
reverseCurve: Curves.ease
)..addStatusListener((PerformanceStatus status) {
if (status == PerformanceStatus.dismissed || status == PerformanceStatus.completed)
reverseValueAnimationDirection();
});
valueAnimation.play(valueAnimationDirection);
}
ValuePerformance<double> valueAnimation;
AnimationDirection valueAnimationDirection = AnimationDirection.forward;
Animation animation;
AnimationController controller;
void handleTap() {
setState(() {
// valueAnimation.isAnimating is part of our build state
if (valueAnimation.isAnimating)
valueAnimation.stop();
if (controller.isAnimating)
controller.stop();
else
valueAnimation.resume();
controller.resume();
});
}
void reverseValueAnimationDirection() {
valueAnimationDirection = (valueAnimationDirection == AnimationDirection.forward)
AnimationDirection direction = (controller.direction == AnimationDirection.forward)
? AnimationDirection.reverse
: AnimationDirection.forward;
valueAnimation.play(valueAnimationDirection);
controller.play(direction);
}
Widget buildIndicators(BuildContext context) {
......@@ -55,19 +54,19 @@ class _ProgressIndicatorAppState extends State<ProgressIndicatorApp> {
),
new LinearProgressIndicator(),
new LinearProgressIndicator(),
new LinearProgressIndicator(value: valueAnimation.value),
new LinearProgressIndicator(value: animation.progress),
new CircularProgressIndicator(),
new SizedBox(
width: 20.0,
height: 20.0,
child: new CircularProgressIndicator(value: valueAnimation.value)
child: new CircularProgressIndicator(value: animation.progress)
),
new SizedBox(
width: 50.0,
height: 30.0,
child: new CircularProgressIndicator(value: valueAnimation.value)
child: new CircularProgressIndicator(value: animation.progress)
),
new Text("${(valueAnimation.value * 100.0).toStringAsFixed(1)}%" + (valueAnimation.isAnimating ? '' : ' (paused)'))
new Text("${(animation.progress * 100.0).toStringAsFixed(1)}%" + (controller.isAnimating ? '' : ' (paused)'))
];
return new Column(
children: indicators
......@@ -82,9 +81,8 @@ class _ProgressIndicatorAppState extends State<ProgressIndicatorApp> {
onTap: handleTap,
child: new Container(
padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
child: new BuilderTransition(
variables: <AnimatedValue<double>>[valueAnimation.variable],
performance: valueAnimation.view,
child: new AnimationWatchingBuilder(
watchable: animation,
builder: buildIndicators
)
)
......
......@@ -16,3 +16,4 @@ export 'src/animation/performance.dart';
export 'src/animation/scroll_behavior.dart';
export 'src/animation/simulation_stepper.dart';
export 'src/animation/ticker.dart';
export 'src/animation/tween.dart';
// Copyright 2016 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:async';
import 'dart:ui' show Color, Size, Rect, VoidCallback, lerpDouble;
import 'package:newton/newton.dart';
import 'animated_value.dart';
import 'curves.dart';
import 'forces.dart';
import 'listener_helpers.dart';
import 'simulation_stepper.dart';
abstract class Watchable {
const Watchable();
/// Calls the listener every time the progress of the performance changes.
void addListener(VoidCallback listener);
/// Stop calling the listener every time the progress of the performance changes.
void removeListener(VoidCallback listener);
/// Calls listener every time the status of the performance changes.
void addStatusListener(PerformanceStatusListener listener);
/// Stops calling the listener every time the status of the performance changes.
void removeStatusListener(PerformanceStatusListener listener);
/// The current status of this animation.
PerformanceStatus get status;
/// The current direction of the animation.
AnimationDirection get direction;
/// The direction used to select the current curve.
///
/// The curve direction is only reset when we hit the beginning or the end of
/// the timeline to avoid discontinuities in the value of any variables this
/// performance is used to animate.
AnimationDirection get curveDirection;
/// Whether this animation is stopped at the beginning.
bool get isDismissed => status == PerformanceStatus.dismissed;
/// Whether this animation is stopped at the end.
bool get isCompleted => status == PerformanceStatus.completed;
String toString() {
return '$runtimeType(${toStringDetails()})';
}
String toStringDetails() {
assert(status != null);
assert(direction != null);
String icon;
switch (status) {
case PerformanceStatus.forward:
icon = '\u25B6'; // >
break;
case PerformanceStatus.reverse:
icon = '\u25C0'; // <
break;
case PerformanceStatus.completed:
switch (direction) {
case AnimationDirection.forward:
icon = '\u23ED'; // >>|
break;
case AnimationDirection.reverse:
icon = '\u29CF'; // <|
break;
}
break;
case PerformanceStatus.dismissed:
switch (direction) {
case AnimationDirection.forward:
icon = '\u29D0'; // |>
break;
case AnimationDirection.reverse:
icon = '\u23EE'; // |<<
break;
}
break;
}
assert(icon != null);
return '$icon';
}
}
abstract class ProxyWatchableMixin implements Watchable {
Watchable get parent;
void addListener(VoidCallback listener) => parent.addListener(listener);
void removeListener(VoidCallback listener) => parent.removeListener(listener);
void addStatusListener(PerformanceStatusListener listener) => parent.addStatusListener(listener);
void removeStatusListener(PerformanceStatusListener listener) => parent.removeStatusListener(listener);
PerformanceStatus get status => parent.status;
AnimationDirection get direction => parent.direction;
AnimationDirection get curveDirection => parent.curveDirection;
}
abstract class Evaluatable<T> {
const Evaluatable();
T evaluate(double t);
WatchableValue<T> watch(Animation parent) {
return new WatchableValue<T>(parent: parent, evaluatable: this);
}
}
class WatchableValue<T> extends Watchable with ProxyWatchableMixin {
WatchableValue({ this.parent, this.evaluatable });
final Animation parent;
final Evaluatable<T> evaluatable;
T get value => evaluatable.evaluate(parent.progress);
}
abstract class Animation extends Watchable {
/// The current progress of this animation (a value from 0.0 to 1.0).
double get progress;
String toStringDetails() {
return '${super.toStringDetails()} ${progress.toStringAsFixed(3)}';
}
}
class AnimationController extends Animation
with EagerListenerMixin, LocalPerformanceListenersMixin, LocalPerformanceStatusListenersMixin {
AnimationController({ this.duration, double progress, this.debugLabel }) {
_timeline = new SimulationStepper(_tick);
if (progress != null)
_timeline.value = progress.clamp(0.0, 1.0);
}
/// A label that is used in the [toString] output. Intended to aid with
/// identifying performance instances in debug output.
final String debugLabel;
/// Returns a [Animation] for this performance,
/// so that a pointer to this object can be passed around without
/// allowing users of that pointer to mutate the Performance state.
Animation get view => this;
/// The length of time this performance should last.
Duration duration;
SimulationStepper _timeline;
AnimationDirection get direction => _direction;
AnimationDirection _direction = AnimationDirection.forward;
AnimationDirection get curveDirection => _curveDirection;
AnimationDirection _curveDirection = AnimationDirection.forward;
/// The progress of this performance along the timeline.
///
/// Note: Setting this value stops the current animation.
double get progress => _timeline.value.clamp(0.0, 1.0);
void set progress(double t) {
stop();
_timeline.value = t.clamp(0.0, 1.0);
_checkStatusChanged();
}
/// Whether this animation is currently animating in either the forward or reverse direction.
bool get isAnimating => _timeline.isAnimating;
PerformanceStatus get status {
if (!isAnimating && progress == 1.0)
return PerformanceStatus.completed;
if (!isAnimating && progress == 0.0)
return PerformanceStatus.dismissed;
return _direction == AnimationDirection.forward ?
PerformanceStatus.forward :
PerformanceStatus.reverse;
}
/// Starts running this animation forwards (towards the end).
Future forward() => play(AnimationDirection.forward);
/// Starts running this animation in reverse (towards the beginning).
Future reverse() => play(AnimationDirection.reverse);
/// Starts running this animation in the given direction.
Future play(AnimationDirection direction) {
_direction = direction;
return resume();
}
/// Resumes this animation in the most recent direction.
Future resume() {
return _animateTo(_direction == AnimationDirection.forward ? 1.0 : 0.0);
}
/// Stops running this animation.
void stop() {
_timeline.stop();
}
/// Releases any resources used by this object.
///
/// Same as stop().
void dispose() {
stop();
}
///
/// Flings the timeline with an optional force (defaults to a critically
/// damped spring) and initial velocity. If velocity is positive, the
/// animation will complete, otherwise it will dismiss.
Future fling({double velocity: 1.0, Force force}) {
force ??= kDefaultSpringForce;
_direction = velocity < 0.0 ? AnimationDirection.reverse : AnimationDirection.forward;
return _timeline.animateWith(force.release(progress, velocity));
}
/// Starts running this animation in the forward direction, and
/// restarts the animation when it completes.
Future repeat({ double min: 0.0, double max: 1.0, Duration period }) {
period ??= duration;
return _timeline.animateWith(new _RepeatingSimulation(min, max, period));
}
PerformanceStatus _lastStatus = PerformanceStatus.dismissed;
void _checkStatusChanged() {
PerformanceStatus currentStatus = status;
if (currentStatus != _lastStatus)
notifyStatusListeners(status);
_lastStatus = currentStatus;
}
void _updateCurveDirection() {
if (status != _lastStatus) {
if (_lastStatus == PerformanceStatus.dismissed || _lastStatus == PerformanceStatus.completed)
_curveDirection = _direction;
}
}
Future _animateTo(double target) {
Duration remainingDuration = duration * (target - _timeline.value).abs();
_timeline.stop();
if (remainingDuration == Duration.ZERO)
return new Future.value();
return _timeline.animateTo(target, duration: remainingDuration);
}
void _tick(double t) {
_updateCurveDirection();
notifyListeners();
_checkStatusChanged();
}
String toStringDetails() {
String paused = _timeline.isAnimating ? '' : '; paused';
String label = debugLabel == null ? '' : '; for $debugLabel';
String more = super.toStringDetails();
return '$more$paused$label';
}
}
class _RepeatingSimulation extends Simulation {
_RepeatingSimulation(this.min, this.max, Duration period)
: _periodInSeconds = period.inMicroseconds / Duration.MICROSECONDS_PER_SECOND {
assert(_periodInSeconds > 0.0);
}
final double min;
final double max;
final double _periodInSeconds;
double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0);
final double t = (timeInSeconds / _periodInSeconds) % 1.0;
return lerpDouble(min, max, t);
}
double dx(double timeInSeconds) => 1.0;
bool isDone(double timeInSeconds) => false;
}
// TODO(abarth): Rename Curve to UnitTransform and ACurve to Curve.
class ACurve extends Animation with ProxyWatchableMixin {
ACurve({ this.parent, this.curve, this.reverseCurve }) {
assert(parent != null);
assert(curve != null);
}
final Animation parent;
/// The curve to use in the forward direction.
Curve curve;
/// The curve to use in the reverse direction.
///
/// If this field is null, uses [curve] in both directions.
Curve reverseCurve;
double get progress {
final bool useForwardCurve = parent.curveDirection == AnimationDirection.forward || reverseCurve == null;
Curve activeCurve = useForwardCurve ? curve : reverseCurve;
double t = parent.progress;
if (activeCurve == null)
return t;
if (t == 0.0 || t == 1.0) {
assert(activeCurve.transform(t).round() == t);
return t;
}
return activeCurve.transform(t);
}
}
class Tween<T extends dynamic> extends Evaluatable<T> {
Tween({ this.begin, this.end }) {
assert(begin != null);
}
/// The value this variable has at the beginning of the animation.
T begin;
/// The value this variable has at the end of the animation.
T end;
/// Returns the value this variable has at the given animation clock value.
T lerp(double t) => begin + (end - begin) * t;
T evaluate(double t) {
if (end == null)
return begin;
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
}
/// An animated variable containing a color.
///
/// This class specializes the interpolation of Tween<Color> to be
/// appropriate for colors.
class ColorTween extends Tween<Color> {
ColorTween({ Color begin, Color end }) : super(begin: begin, end: end);
Color lerp(double t) => Color.lerp(begin, end, t);
}
/// An animated variable containing a size.
///
/// This class specializes the interpolation of Tween<Size> to be
/// appropriate for rectangles.
class SizeTween extends Tween<Size> {
SizeTween({ Size begin, Size end }) : super(begin: begin, end: end);
Size lerp(double t) => Size.lerp(begin, end, t);
}
/// An animated variable containing a rectangle.
///
/// This class specializes the interpolation of Tween<Rect> to be
/// appropriate for rectangles.
class RectTween extends Tween<Rect> {
RectTween({ Rect begin, Rect end }) : super(begin: begin, end: end);
Rect lerp(double t) => Rect.lerp(begin, end, t);
}
/// An animated variable containing a int.
class IntTween extends Tween<int> {
IntTween({ int begin, int end }) : super(begin: begin, end: end);
// The inherited lerp() function doesn't work with ints because it multiplies
// the begin and end types by a double, and int * double returns a double.
int lerp(double t) => (begin + (end - begin) * t).round();
}
......@@ -63,6 +63,55 @@ class _TransitionState extends State<TransitionComponent> {
}
}
abstract class AnimationWatchingComponent extends StatefulComponent {
AnimationWatchingComponent({
Key key,
this.watchable
}) : super(key: key) {
assert(watchable != null);
}
final Watchable watchable;
Widget build(BuildContext context);
_AnimationWatchingComponentState createState() => new _AnimationWatchingComponentState();
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('watchable: $watchable');
}
}
class _AnimationWatchingComponentState extends State<AnimationWatchingComponent> {
void initState() {
super.initState();
config.watchable.addListener(_handleTick);
}
void didUpdateConfig(AnimationWatchingComponent oldConfig) {
if (config.watchable != oldConfig.watchable) {
oldConfig.watchable.removeListener(_handleTick);
config.watchable.addListener(_handleTick);
}
}
void dispose() {
config.watchable.removeListener(_handleTick);
super.dispose();
}
void _handleTick() {
setState(() {
// The watchable's state is our build state, and it changed already.
});
}
Widget build(BuildContext context) {
return config.build(context);
}
}
abstract class TransitionWithChild extends TransitionComponent {
TransitionWithChild({
Key key,
......@@ -284,7 +333,6 @@ class PositionedTransition extends TransitionWithChild {
}
}
class BuilderTransition extends TransitionComponent {
BuilderTransition({
Key key,
......@@ -303,3 +351,17 @@ class BuilderTransition extends TransitionComponent {
return builder(context);
}
}
class AnimationWatchingBuilder extends AnimationWatchingComponent {
AnimationWatchingBuilder({
Key key,
Watchable watchable,
this.builder
}) : super(key: key, watchable: watchable);
final WidgetBuilder builder;
Widget build(BuildContext context) {
return builder(context);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment