From 688a084e861de2a7fb11ffda1b68751f564428f0 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 10 Jul 2015 15:48:03 -0700 Subject: [PATCH] Switch scroll physics over to using newton The scroll physics was the last client of the old frame generator. With this change, we've fully switched over to the new animation system. R=chinmaygarde@google.com Review URL: https://codereview.chromium.org/1226133004 . --- sdk/BUILD.gn | 3 +- sdk/example/widgets/card_collection.dart | 8 +- sdk/lib/animation/animated_simulation.dart | 60 ++++++ sdk/lib/animation/generators.dart | 95 --------- sdk/lib/animation/mechanics.dart | 213 ------------------- sdk/lib/animation/scroll_behavior.dart | 120 ++++------- sdk/lib/widgets/fixed_height_scrollable.dart | 4 +- sdk/lib/widgets/scrollable.dart | 36 ++-- sdk/lib/widgets/tabs.dart | 33 +-- 9 files changed, 137 insertions(+), 435 deletions(-) create mode 100644 sdk/lib/animation/animated_simulation.dart delete mode 100644 sdk/lib/animation/generators.dart delete mode 100644 sdk/lib/animation/mechanics.dart 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); }); }