diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index cd38020c4cd..7446a11b326 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -8,11 +8,10 @@ dart_pkg("sky") { sources = [ "CHANGELOG.md", "bin/init.dart", + "lib/animation/animated_simulation.dart", "lib/animation/animation_performance.dart", "lib/animation/curves.dart", "lib/animation/fling_curve.dart", - "lib/animation/generators.dart", - "lib/animation/mechanics.dart", "lib/animation/scroll_behavior.dart", "lib/animation/timeline.dart", "lib/assets/.gitignore", diff --git a/sdk/example/widgets/card_collection.dart b/sdk/example/widgets/card_collection.dart index e20648c21fe..ef6d82d8fc5 100644 --- a/sdk/example/widgets/card_collection.dart +++ b/sdk/example/widgets/card_collection.dart @@ -47,8 +47,8 @@ class VariableHeightScrollable extends Scrollable { void _handleSizeChanged(Size newSize) { setState(() { - scrollBehavior.containerHeight = newSize.height; - scrollBehavior.contentsHeight = 5000.0; + scrollBehavior.containerSize = newSize.height; + scrollBehavior.contentsSize = 5000.0; }); } @@ -70,13 +70,13 @@ class CardCollectionApp extends App { new TextStyle(color: White, fontSize: 18.0, fontWeight: bold); final List cardHeights = [ - 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, + 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0 ]; List visibleCardIndices; - + CardCollectionApp() { _activeCardTransform = new AnimatedContainer() ..position = new AnimatedType(Point.origin) diff --git a/sdk/lib/animation/animated_simulation.dart b/sdk/lib/animation/animated_simulation.dart new file mode 100644 index 00000000000..38c58354225 --- /dev/null +++ b/sdk/lib/animation/animated_simulation.dart @@ -0,0 +1,60 @@ +// 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:async'; + +import 'package:newton/newton.dart'; + +import 'timeline.dart'; + +const double _kSecondsPerMillisecond = 1000.0; + +class AnimatedSimulation { + + AnimatedSimulation(Function onTick) : _onTick = onTick { + _ticker = new Ticker(_tick); + } + + final Function _onTick; + Ticker _ticker; + + Simulation _simulation; + double _startTime; + + double _value = 0.0; + double get value => _value; + + Future start(Simulation simulation) { + assert(simulation != null); + assert(!_ticker.isTicking); + _simulation = simulation; + _startTime = null; + _value = simulation.x(0.0); + return _ticker.start(); + } + + void stop() { + _simulation = null; + _startTime = null; + _value = 0.0; + _ticker.stop(); + } + + bool get isAnimating => _ticker.isTicking; + + void _tick(double timeStamp) { + if (_startTime == null) + _startTime = timeStamp; + + double timeInSeconds = (timeStamp - _startTime) / _kSecondsPerMillisecond; + _value = _simulation.x(timeInSeconds); + final bool isLastTick = _simulation.isDone(timeInSeconds); + + _onTick(_value); + + if (isLastTick) + stop(); + } + +} diff --git a/sdk/lib/animation/generators.dart b/sdk/lib/animation/generators.dart deleted file mode 100644 index fdec2df47ca..00000000000 --- a/sdk/lib/animation/generators.dart +++ /dev/null @@ -1,95 +0,0 @@ -// 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:async'; - -import '../base/scheduler.dart' as scheduler; -import 'mechanics.dart'; - -abstract class Generator { - Stream get onTick; // TODO(ianh): rename this to tickStream - void cancel(); -} - -class FrameGenerator extends Generator { - Function onDone; - StreamController _controller; - - Stream get onTick => _controller.stream; - - int _animationId = 0; - bool _cancelled = false; - - FrameGenerator({this.onDone}) { - _controller = new StreamController( - sync: true, - onListen: _scheduleTick, - onCancel: cancel); - } - - void cancel() { - if (_cancelled) { - return; - } - if (_animationId != 0) { - scheduler.cancelAnimationFrame(_animationId); - } - _animationId = 0; - _cancelled = true; - if (onDone != null) { - onDone(); - } - } - - void _scheduleTick() { - assert(_animationId == 0); - _animationId = scheduler.requestAnimationFrame(_tick); - } - - void _tick(double timeStamp) { - _animationId = 0; - _controller.add(timeStamp); - if (!_cancelled) { - _scheduleTick(); - } - } -} - -class Simulation extends Generator { - Stream get onTick => _stream; - final System system; - - FrameGenerator _generator; - Stream _stream; - double _previousTime = 0.0; - - Simulation(this.system, {Function terminationCondition, Function onDone}) { - _generator = new FrameGenerator(onDone: onDone); - _stream = _generator.onTick.map(_update); - - if (terminationCondition != null) { - bool done = false; - _stream = _stream.takeWhile((_) { - if (done) - return false; - done = terminationCondition(); - return true; - }); - } - } - - void cancel() { - _generator.cancel(); - } - - double _update(double timeStamp) { - double previousTime = _previousTime; - _previousTime = timeStamp; - if (previousTime == 0.0) - return timeStamp; - double deltaT = timeStamp - previousTime; - system.update(deltaT); - return timeStamp; - } -} diff --git a/sdk/lib/animation/mechanics.dart b/sdk/lib/animation/mechanics.dart deleted file mode 100644 index 6a6cc4cad46..00000000000 --- a/sdk/lib/animation/mechanics.dart +++ /dev/null @@ -1,213 +0,0 @@ -// 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:math' as math; - -const double kGravity = -0.980; // m^s-2 - -abstract class System { - void update(double deltaT); -} - -class Particle extends System { - final double mass; - double velocity; - double position; - - Particle({this.mass: 1.0, this.velocity: 0.0, this.position: 0.0}); - - void applyImpulse(double impulse) { - velocity += impulse / mass; - } - - void update(double deltaT) { - position += velocity * deltaT; - } - - void setVelocityFromEnergy({double energy, double direction}) { - assert(direction == -1.0 || direction == 1.0); - assert(energy >= 0.0); - velocity = math.sqrt(2.0 * energy / mass) * direction; - } -} - -abstract class Box { - void confine(Particle p); -} - -class ClosedBox extends Box { - final double min; // m - final double max; // m - - ClosedBox({this.min, this.max}) { - assert(min == null || max == null || min <= max); - } - - void confine(Particle p) { - if (min != null) { - p.position = math.max(min, p.position); - if (p.position == min) - p.velocity = math.max(0.0, p.velocity); - } - if (max != null) { - p.position = math.min(max, p.position); - if (p.position == max) - p.velocity = math.min(0.0, p.velocity); - } - } -} - -class GeofenceBox extends Box { - final double min; // m - final double max; // m - - final Function onEscape; - - GeofenceBox({this.min, this.max, this.onEscape}) { - assert(min == null || max == null || min <= max); - assert(onEscape != null); - } - - void confine(Particle p) { - if (((min != null) && (p.position < min)) || - ((max != null) && (p.position > max))) - onEscape(); - } -} - -class ParticleInBox extends System { - final Particle particle; - final Box box; - - ParticleInBox({this.particle, this.box}) { - box.confine(particle); - } - - void update(double deltaT) { - particle.update(deltaT); - box.confine(particle); - } -} - -class ParticleInBoxWithFriction extends ParticleInBox { - final double friction; // unitless - final double _sign; - - final Function onStop; - - ParticleInBoxWithFriction({Particle particle, Box box, this.friction, this.onStop}) - : super(particle: particle, box: box), - _sign = particle.velocity.sign; - - void update(double deltaT) { - double force = -_sign * friction * particle.mass * -kGravity; - particle.applyImpulse(force * deltaT); - if (particle.velocity.sign != _sign) { - particle.velocity = 0.0; - } - super.update(deltaT); - if ((particle.velocity == 0.0) && (onStop != null)) - onStop(); - } -} - -class Spring { - final double k; - double displacement; - - Spring(this.k, {this.displacement: 0.0}); - - double get force => -k * displacement; -} - -class ParticleAndSpringInBox extends System { - final Particle particle; - final Spring spring; - final Box box; - - ParticleAndSpringInBox({this.particle, this.spring, this.box}) { - _applyInvariants(); - } - - void update(double deltaT) { - particle.applyImpulse(spring.force * deltaT); - particle.update(deltaT); - _applyInvariants(); - } - - void _applyInvariants() { - box.confine(particle); - spring.displacement = particle.position; - } -} - -class ParticleClimbingRamp extends System { - - // This is technically the same as ParticleInBoxWithFriction. The - // difference is in how the system is set up. Here, we configure the - // system so as to stop by a certain distance after having been - // given an initial impulse from rest, whereas - // ParticleInBoxWithFriction is set up to stop with a consistent - // decelerating force assuming an initial velocity. The angle theta - // (0 < theta < π/2) is used to configure how much energy the - // particle is to start with; lower angles result in a gentler kick - // while higher angles result in a faster conclusion. - - final Particle particle; - final Box box; - final double theta; - final double _sinTheta; - - ParticleClimbingRamp({ - this.particle, - this.box, - double theta, // in radians - double targetPosition}) : this.theta = theta, this._sinTheta = math.sin(theta) { - assert(theta > 0.0); - assert(theta < math.PI / 2.0); - double deltaPosition = targetPosition - particle.position; - double tanTheta = math.tan(theta); - // We need to give the particle exactly as much (kinetic) energy - // as it needs to get to the top of the slope and stop with - // energy=0. This is exactly the same amount of energy as the - // potential energy at the top of the slope, which is g*h*m. - // If the slope's horizontal component is delta P long, then - // the height is delta P times tan theta. - particle.setVelocityFromEnergy( - energy: (kGravity * (deltaPosition * tanTheta) * particle.mass).abs(), - direction: deltaPosition > 0.0 ? 1.0 : -1.0 - ); - box.confine(particle); - } - - void update(double deltaT) { - particle.update(deltaT); - // Note that we apply the impulse from gravity after updating the particle's - // position so that we overestimate the distance traveled by the particle. - // That ensures that we actually hit the edge of the box and don't wind up - // reversing course. - particle.applyImpulse(particle.mass * kGravity * _sinTheta * deltaT); - box.confine(particle); - } -} - -class Multisystem extends System { - final Particle particle; - - System _currentSystem; - - Multisystem({ this.particle, System system }) { - assert(system != null); - _currentSystem = system; - } - - void update(double deltaT) { - _currentSystem.update(deltaT); - } - - void transitionToSystem(System system) { - assert(system != null); - _currentSystem = system; - } -} diff --git a/sdk/lib/animation/scroll_behavior.dart b/sdk/lib/animation/scroll_behavior.dart index b07ee8f7c6d..b69c05db0ab 100644 --- a/sdk/lib/animation/scroll_behavior.dart +++ b/sdk/lib/animation/scroll_behavior.dart @@ -4,97 +4,71 @@ import 'dart:math' as math; -import 'mechanics.dart'; -import 'generators.dart'; +import 'package:newton/newton.dart'; -const double _kScrollFriction = 0.005; -const double _kOverscrollFriction = 0.075; -const double _kBounceSlopeAngle = math.PI / 512.0; // radians +const double _kSecondsPerMillisecond = 1000.0; abstract class ScrollBehavior { - Simulation release(Particle particle) => null; + Simulation release(double position, double velocity) => null; // Returns the new scroll offset. double applyCurve(double scrollOffset, double scrollDelta); } -class BoundedScrollBehavior extends ScrollBehavior { - double minOffset; - double maxOffset; +class BoundedBehavior extends ScrollBehavior { + BoundedBehavior({ double contentsSize: 0.0, double containerSize: 0.0 }) + : _contentsSize = contentsSize, + _containerSize = containerSize; - BoundedScrollBehavior({this.minOffset: 0.0, this.maxOffset}); + double _contentsSize; + double get contentsSize => _contentsSize; + void set contentsSize (double value) { + if (_contentsSize != value) { + _contentsSize = value; + // TODO(ianh) now what? what if we have a simulation ongoing? + } + } + + double _containerSize; + double get containerSize => _containerSize; + void set containerSize (double value) { + if (_containerSize != value) { + _containerSize = value; + // TODO(ianh) now what? what if we have a simulation ongoing? + } + } + + final double minScrollOffset = 0.0; + double get maxScrollOffset => math.max(0.0, _contentsSize - _containerSize); double applyCurve(double scrollOffset, double scrollDelta) { - double newScrollOffset = scrollOffset + scrollDelta; - if (minOffset != null) - newScrollOffset = math.max(minOffset, newScrollOffset); - if (maxOffset != null) - newScrollOffset = math.min(maxOffset, newScrollOffset); - return newScrollOffset; + return (scrollOffset + scrollDelta).clamp(0.0, maxScrollOffset); } } -class OverscrollBehavior extends ScrollBehavior { +Simulation createDefaultScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) { + double velocityPerSecond = velocity * _kSecondsPerMillisecond; + SpringDescription spring = new SpringDescription.withDampingRatio( + mass: 1.0, springConstant: 85.0, ratio: 1.1); + double drag = 0.4; + return new ScrollSimulation(position, velocityPerSecond, minScrollOffset, maxScrollOffset, spring, drag); +} - double _contentsHeight; - double get contentsHeight => _contentsHeight; - void set contentsHeight (double value) { - if (_contentsHeight != value) { - _contentsHeight = value; - // TODO(ianh) now what? what if we have a simulation ongoing? - } +class FlingBehavior extends BoundedBehavior { + FlingBehavior({ double contentsSize: 0.0, double containerSize: 0.0 }) + : super(contentsSize: contentsSize, containerSize: containerSize); + + Simulation release(double position, double velocity) { + return createDefaultScrollSimulation(position, 0.0, minScrollOffset, maxScrollOffset); } +} - double _containerHeight; - double get containerHeight => _containerHeight; - void set containerHeight (double value) { - if (_containerHeight != value) { - _containerHeight = value; - // TODO(ianh) now what? what if we have a simulation ongoing? - } - } +class OverscrollBehavior extends BoundedBehavior { + OverscrollBehavior({ double contentsSize: 0.0, double containerSize: 0.0 }) + : super(contentsSize: contentsSize, containerSize: containerSize); - OverscrollBehavior({double contentsHeight: 0.0, double containerHeight: 0.0}) - : _contentsHeight = contentsHeight, - _containerHeight = containerHeight; - - double get maxScrollOffset => math.max(0.0, _contentsHeight - _containerHeight); - - Simulation release(Particle particle) { - System system; - if ((particle.position >= 0.0) && (particle.position < maxScrollOffset)) { - if (particle.velocity == 0.0) - return null; - System slowdownSystem = new ParticleInBoxWithFriction( - particle: particle, - friction: _kScrollFriction, - box: new GeofenceBox(min: 0.0, max: maxScrollOffset, onEscape: () { - (system as Multisystem).transitionToSystem(new ParticleInBoxWithFriction( - particle: particle, - friction: _kOverscrollFriction, - box: new ClosedBox(), - onStop: () => (system as Multisystem).transitionToSystem(getBounceBackSystem(particle)) - )); - })); - system = new Multisystem(particle: particle, system: slowdownSystem); - } else { - system = getBounceBackSystem(particle); - } - return new Simulation(system, terminationCondition: () => particle.position == 0.0); - } - - System getBounceBackSystem(Particle particle) { - if (particle.position < 0.0) - return new ParticleClimbingRamp( - particle: particle, - box: new ClosedBox(max: 0.0), - theta: _kBounceSlopeAngle, - targetPosition: 0.0); - return new ParticleClimbingRamp( - particle: particle, - box: new ClosedBox(min: maxScrollOffset), - theta: _kBounceSlopeAngle, - targetPosition: maxScrollOffset); + Simulation release(double position, double velocity) { + return createDefaultScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset); } double applyCurve(double scrollOffset, double scrollDelta) { diff --git a/sdk/lib/widgets/fixed_height_scrollable.dart b/sdk/lib/widgets/fixed_height_scrollable.dart index e4dc1f4c7f7..bc81eacfda6 100644 --- a/sdk/lib/widgets/fixed_height_scrollable.dart +++ b/sdk/lib/widgets/fixed_height_scrollable.dart @@ -38,7 +38,7 @@ abstract class FixedHeightScrollable extends Scrollable { void _handleSizeChanged(Size newSize) { setState(() { _height = newSize.height; - scrollBehavior.containerHeight = _height; + scrollBehavior.containerSize = _height; }); } @@ -46,7 +46,7 @@ abstract class FixedHeightScrollable extends Scrollable { double contentsHeight = itemHeight * itemCount; if (padding != null) contentsHeight += padding.top + padding.bottom; - scrollBehavior.contentsHeight = contentsHeight; + scrollBehavior.contentsSize = contentsHeight; } void _updateScrollOffset() { diff --git a/sdk/lib/widgets/scrollable.dart b/sdk/lib/widgets/scrollable.dart index 6a8beb29c53..b9eab727001 100644 --- a/sdk/lib/widgets/scrollable.dart +++ b/sdk/lib/widgets/scrollable.dart @@ -4,8 +4,9 @@ import 'dart:sky' as sky; -import '../animation/generators.dart'; -import '../animation/mechanics.dart'; +import 'package:newton/newton.dart'; + +import '../animation/animated_simulation.dart'; import '../animation/scroll_behavior.dart'; import '../theme/view_configuration.dart' as config; import 'basic.dart'; @@ -30,7 +31,9 @@ abstract class Scrollable extends StatefulComponent { String key, this.backgroundColor, this.direction: ScrollDirection.vertical - }) : super(key: key); + }) : super(key: key) { + _animation = new AnimatedSimulation(_tickScrollOffset); + } Color backgroundColor; ScrollDirection direction; @@ -51,7 +54,7 @@ abstract class Scrollable extends StatefulComponent { return _scrollBehavior; } - Simulation _simulation; + AnimatedSimulation _animation; Widget buildContent(); @@ -123,26 +126,23 @@ abstract class Scrollable extends StatefulComponent { } void settleScrollOffset() { - _startSimulation(_createParticle()); + _startSimulation(); } void _stopSimulation() { - if (_simulation == null) - return; - _simulation.cancel(); - _simulation = null; + _animation.stop(); } - void _startSimulation(Particle particle) { + void _startSimulation({ double velocity: 0.0 }) { _stopSimulation(); - _simulation = scrollBehavior.release(particle); - if (_simulation == null) - return; - _simulation.onTick.listen((_) => scrollTo(particle.position)); + print("velocity=$velocity"); + Simulation simulation = scrollBehavior.release(scrollOffset, velocity); + if (simulation != null) + _animation.start(simulation); } - Particle _createParticle([double velocity = 0.0]) { - return new Particle(position: _scrollOffset, velocity: velocity); + void _tickScrollOffset(double value) { + scrollTo(value); } void _handlePointerDown(_) { @@ -150,7 +150,7 @@ abstract class Scrollable extends StatefulComponent { } void _handlePointerUpOrCancel(_) { - if (_simulation == null) + if (!_animation.isAnimating) settleScrollOffset(); } @@ -162,7 +162,7 @@ abstract class Scrollable extends StatefulComponent { double eventVelocity = direction == ScrollDirection.horizontal ? -event.velocityX : -event.velocityY; - _startSimulation(_createParticle(_velocityForFlingGesture(eventVelocity))); + _startSimulation(velocity: _velocityForFlingGesture(eventVelocity)); } void _handleFlingCancel(sky.GestureEvent event) { diff --git a/sdk/lib/widgets/tabs.dart b/sdk/lib/widgets/tabs.dart index 89917b594e9..bb12080c7de 100644 --- a/sdk/lib/widgets/tabs.dart +++ b/sdk/lib/widgets/tabs.dart @@ -4,13 +4,10 @@ import 'dart:math' as math; -import 'package:sky/animation/generators.dart'; -import 'package:sky/animation/mechanics.dart'; import 'package:sky/animation/scroll_behavior.dart'; import 'package:sky/painting/text_style.dart'; import 'package:sky/rendering/box.dart'; import 'package:sky/rendering/object.dart'; -import 'package:vector_math/vector_math.dart'; import 'package:sky/theme/colors.dart' as colors; import 'package:sky/theme/typography.dart' as typography; import 'package:sky/widgets/basic.dart'; @@ -20,6 +17,7 @@ import 'package:sky/widgets/ink_well.dart'; import 'package:sky/widgets/scrollable.dart'; import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/widget.dart'; +import 'package:vector_math/vector_math.dart'; typedef void SelectedIndexChanged(int selectedIndex); typedef void LayoutChanged(Size size, List widths); @@ -347,27 +345,6 @@ class Tab extends Component { } } -class TabBarScrollBehavior extends ScrollBehavior { - TabBarScrollBehavior({ this.maxScrollOffset: 0.0 }); - - double maxScrollOffset; - - Simulation release(Particle particle) { - if (particle.velocity == 0.0 || particle.position < 0.0 || particle.position >= maxScrollOffset) - return null; - - System system = new ParticleInBoxWithFriction( - particle: particle, - friction: _kTabBarScrollFriction, - box: new ClosedBox(min: 0.0, max: maxScrollOffset)); - return new Simulation(system, terminationCondition: () => particle.position == 0.0); - } - - double applyCurve(double scrollOffset, double scrollDelta) { - return (scrollOffset + scrollDelta).clamp(0.0, maxScrollOffset); - } -} - class TabBar extends Scrollable { TabBar({ String key, @@ -392,8 +369,8 @@ class TabBar extends Scrollable { scrollTo(0.0); } - ScrollBehavior createScrollBehavior() => new TabBarScrollBehavior(); - TabBarScrollBehavior get scrollBehavior => super.scrollBehavior; + ScrollBehavior createScrollBehavior() => new FlingBehavior(); + FlingBehavior get scrollBehavior => super.scrollBehavior; void _handleTap(int tabIndex) { if (tabIndex != selectedIndex && onChanged != null) @@ -419,8 +396,8 @@ class TabBar extends Scrollable { setState(() { _tabBarSize = tabBarSize; _tabWidths = tabWidths; - scrollBehavior.maxScrollOffset = - _tabWidths.reduce((sum, width) => sum + width) - _tabBarSize.width; + scrollBehavior.containerSize = _tabBarSize.width; + scrollBehavior.contentsSize = _tabWidths.reduce((sum, width) => sum + width); }); }