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 .
This commit is contained in:
Adam Barth 2015-07-10 15:48:03 -07:00
parent 93a94f315b
commit 688a084e86
9 changed files with 137 additions and 435 deletions

View File

@ -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",

View File

@ -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<double> 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<int> visibleCardIndices;
CardCollectionApp() {
_activeCardTransform = new AnimatedContainer()
..position = new AnimatedType<Point>(Point.origin)

View File

@ -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();
}
}

View File

@ -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<double> get onTick; // TODO(ianh): rename this to tickStream
void cancel();
}
class FrameGenerator extends Generator {
Function onDone;
StreamController _controller;
Stream<double> 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<double> get onTick => _stream;
final System system;
FrameGenerator _generator;
Stream<double> _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;
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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) {

View File

@ -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<double> 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);
});
}