mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
1dd5fac787
commit
06bfddff81
@ -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;
|
||||
|
||||
126
framework/animation/mechanics.dart
Normal file
126
framework/animation/mechanics.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
45
framework/animation/simulation.dart
Normal file
45
framework/animation/simulation.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user