Drive overscroll animations with a physics simulation

This CL uses a simple physics simulation to drive overscroll animations.
We model settling the overscroll as a particle climbing a hill, which
gives us a pleasing parabolic trajectory.

This CL also includes machinery for spring-based simulations. We'll use
these to drive the drawer animation.

R=eseidel@chromium.org

Review URL: https://codereview.chromium.org/999423004
This commit is contained in:
Adam Barth 2015-03-13 09:31:10 -07:00
parent 1dd5fac787
commit 06bfddff81
5 changed files with 220 additions and 33 deletions

View File

@ -51,13 +51,15 @@ class FrameGenerator {
}
}
class AnimationGenerator extends FrameGenerator {
class AnimationGenerator {
Stream<double> get onTick => _stream;
final double initialDelay;
final double duration;
final double begin;
final double end;
final Curve curve;
FrameGenerator _generator;
Stream<double> _stream;
bool _done = false;
@ -68,21 +70,27 @@ class AnimationGenerator extends FrameGenerator {
this.end: 1.0,
this.curve: linear,
Function onDone
}):super(onDone: onDone) {
}) {
assert(duration != null && duration > 0.0);
_generator = new FrameGenerator(onDone: onDone);
double startTime = 0.0;
_stream = super.onTick.map((timeStamp) {
_stream = _generator.onTick.map((timeStamp) {
if (startTime == 0.0)
startTime = timeStamp;
double t = (timeStamp - (startTime + initialDelay)) / duration;
return math.max(0.0, math.min(t, 1.0));
})
.takeWhile(_checkForCompletion) //
.takeWhile(_checkForCompletion)
.where((t) => t >= 0.0)
.map(_transform);
}
void cancel() {
_generator.cancel();
}
double _transform(double t) {
if (_done)
return end;

View File

@ -0,0 +1,126 @@
// 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;
const double _kMinVelocity = 0.1;
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 applyImpluse(double impluse) {
velocity += impluse / mass;
}
void update(double deltaT) {
position += velocity * deltaT;
}
double get energy => 0.5 * mass * velocity * velocity;
set energy(double e) {
assert(e >= 0.0);
velocity = math.sqrt(2 * e / mass);
}
}
class Box {
final double min;
final double max;
Box({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 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 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.applyImpluse(spring.force * deltaT);
particle.update(deltaT);
_applyInvariants();
}
void _applyInvariants() {
box.confine(particle);
spring.displacement = particle.position;
}
}
class ParticleClimbingRamp extends System {
final Particle particle;
final Box box;
final double slope;
ParticleClimbingRamp({
this.particle,
this.box,
this.slope,
double targetPosition}) {
double deltaPosition = targetPosition - particle.position;
particle.energy = -kGravity * slope * deltaPosition * particle.mass;
box.confine(particle);
}
void update(double deltaT) {
particle.applyImpluse(kGravity * slope * deltaT);
// If we don't apply a min velocity, error terms in the simulation can
// prevent us from reaching the targetPosition before gravity overtakes our
// initial velocity and we start rolling down the hill.
particle.velocity = math.max(_kMinVelocity, particle.velocity);
particle.update(deltaT);
box.confine(particle);
}
}

View File

@ -3,8 +3,14 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'mechanics.dart';
import 'simulation.dart';
const double _kSlope = 0.01;
abstract class ScrollCurve {
Simulation release(Particle particle) => null;
// Returns the new scroll offset.
double apply(double scrollOffset, double scrollDelta);
}
@ -26,6 +32,18 @@ class BoundedScrollCurve extends ScrollCurve {
}
class OverscrollCurve extends ScrollCurve {
Simulation release(Particle particle) {
if (particle.position >= 0.0)
return null;
System system = new ParticleClimbingRamp(
particle: particle,
box: new Box(max: 0.0),
slope: _kSlope,
targetPosition: 0.0);
return new Simulation(system,
terminationCondition: () => particle.position == 0.0);
}
double apply(double scrollOffset, double scrollDelta) {
double newScrollOffset = scrollOffset + scrollDelta;
if (newScrollOffset < 0.0) {

View File

@ -0,0 +1,45 @@
// 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 'generator.dart';
import 'mechanics.dart';
class Simulation {
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

@ -6,6 +6,8 @@ import '../animation/curves.dart';
import '../animation/fling_curve.dart';
import '../animation/generator.dart';
import '../animation/scroll_curve.dart';
import '../animation/mechanics.dart';
import '../animation/simulation.dart';
import '../fn.dart';
import 'dart:sky' as sky;
@ -16,7 +18,7 @@ abstract class Scrollable extends Component {
double _scrollOffset = 0.0;
FlingCurve _flingCurve;
int _flingAnimationId;
AnimationGenerator _scrollAnimation;
Simulation _simulation;
Scrollable({Object key, this.scrollCurve}) : super(key: key) {
events.listen('pointerdown', _handlePointerDown);
@ -31,7 +33,7 @@ abstract class Scrollable extends Component {
void didUnmount() {
super.didUnmount();
_stopFling();
_stopScrollAnimation();
_stopSimulation();
}
bool scrollBy(double scrollDelta) {
@ -44,25 +46,6 @@ abstract class Scrollable extends Component {
return true;
}
void animateScrollTo(double targetScrollOffset, {
double initialDelay: 0.0,
double duration: 0.0,
Curve curve: linear}) {
_stopScrollAnimation();
_scrollAnimation = new AnimationGenerator(
duration: duration,
begin: _scrollOffset,
end: targetScrollOffset,
initialDelay: initialDelay,
curve: curve);
_scrollAnimation.onTick.listen((newScrollOffset) {
if (!scrollBy(newScrollOffset - _scrollOffset))
_stopScrollAnimation();
}, onDone: () {
_scrollAnimation = null;
});
}
void _scheduleFlingUpdate() {
_flingAnimationId = sky.window.requestAnimationFrame(_updateFling);
}
@ -75,11 +58,11 @@ abstract class Scrollable extends Component {
_flingAnimationId = null;
}
void _stopScrollAnimation() {
if (_scrollAnimation == null)
void _stopSimulation() {
if (_simulation == null)
return;
_scrollAnimation.cancel();
_scrollAnimation = null;
_simulation.cancel();
_simulation = null;
}
void _updateFling(double timeStamp) {
@ -91,13 +74,20 @@ abstract class Scrollable extends Component {
void _settle() {
_stopFling();
if (_scrollOffset < 0.0)
animateScrollTo(0.0, duration: 200.0, curve: easeOut);
Particle particle = new Particle(position: scrollOffset);
_simulation = scrollCurve.release(particle);
if (_simulation == null)
return;
_simulation.onTick.listen((_) {
setState(() {
_scrollOffset = particle.position;
});
});
}
void _handlePointerDown(_) {
_stopFling();
_stopScrollAnimation();
_stopSimulation();
}
void _handlePointerUpOrCancel(_) {
@ -110,7 +100,7 @@ abstract class Scrollable extends Component {
}
void _handleFlingStart(sky.GestureEvent event) {
_stopScrollAnimation();
_stopSimulation();
_flingCurve = new FlingCurve(-event.velocityY, event.timeStamp);
_scheduleFlingUpdate();
}