Commit 7f95d9b5 authored by Adam Barth's avatar Adam Barth

Merge pull request #1303 from abarth/tween2

Introduce Tween and the new animation API
parents 548dbf08 bc20871c
......@@ -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