Commit 4ca447b6 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Refactored the iOS Scrolling Simulation (#9754)

parent 46105468
...@@ -12,7 +12,6 @@ export 'src/physics/clamped_simulation.dart'; ...@@ -12,7 +12,6 @@ export 'src/physics/clamped_simulation.dart';
export 'src/physics/friction_simulation.dart'; export 'src/physics/friction_simulation.dart';
export 'src/physics/gravity_simulation.dart'; export 'src/physics/gravity_simulation.dart';
export 'src/physics/simulation.dart'; export 'src/physics/simulation.dart';
export 'src/physics/simulation_group.dart';
export 'src/physics/spring_simulation.dart'; export 'src/physics/spring_simulation.dart';
export 'src/physics/tolerance.dart'; export 'src/physics/tolerance.dart';
export 'src/physics/utils.dart'; export 'src/physics/utils.dart';
...@@ -71,6 +71,20 @@ class FrictionSimulation extends Simulation { ...@@ -71,6 +71,20 @@ class FrictionSimulation extends Simulation {
@override @override
double dx(double time) => _v * math.pow(_drag, time); double dx(double time) => _v * math.pow(_drag, time);
/// The value of [x] at `double.INFINITY`.
double get finalX => _x - _v / _dragLog;
/// The time at which the value of `x(time)` will equal [x].
///
/// Returns `double.INFINITY` if the simulation will never reach [x].
double timeAtX(double x) {
if (x == _x)
return 0.0;
if (_v == 0.0 || (_v > 0 ? (x < _x || x > finalX) : (x > _x || x < finalX)))
return double.INFINITY;
return math.log(_dragLog * (x - _x) / _v + 1.0) / _dragLog;
}
@override @override
bool isDone(double time) => dx(time).abs() < tolerance.velocity; bool isDone(double time) => dx(time).abs() < tolerance.velocity;
} }
......
// Copyright 2016 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 'simulation.dart';
import 'tolerance.dart';
import 'utils.dart';
/// Base class for composite simulations.
///
/// Concrete subclasses must implement the [currentSimulation] getter, the
/// [currentIntervalOffset] getter, and the [step] function to select the
/// appropriate simulation at a given time interval. This class implements the
/// [x], [dx], and [isDone] functions by calling the [step] method if necessary
/// and then deferring to the [currentSimulation]'s methods with a time offset
/// by [currentIntervalOffset].
///
/// The tolerance of this simulation is pushed to the simulations that are used
/// by this group as they become active. This mean simulations should not be
/// shared among different groups that are active at the same time.
abstract class SimulationGroup extends Simulation {
/// Initializes the [tolerance] field for subclasses.
SimulationGroup({ Tolerance tolerance: Tolerance.defaultTolerance }) : super(tolerance: tolerance);
/// The currently active simulation.
///
/// This getter should return the same value until [step] is called and
/// returns true.
Simulation get currentSimulation;
/// The time offset applied to the currently active simulation when deferring
/// [x], [dx], and [isDone] to it.
double get currentIntervalOffset;
/// Called when a significant change in the interval is detected. Subclasses
/// must decide if the current simulation must be switched (or updated).
///
/// Must return true if the simulation was switched in this step, otherwise
/// false.
///
/// If this function returns true, then [currentSimulation] must start
/// returning a new value.
bool step(double time);
double _lastStep = -1.0;
void _stepIfNecessary(double time) {
if (nearEqual(_lastStep, time, Tolerance.defaultTolerance.time))
return;
_lastStep = time;
if (step(time))
currentSimulation.tolerance = tolerance;
}
@override
double x(double time) {
_stepIfNecessary(time);
return currentSimulation.x(time - currentIntervalOffset);
}
@override
double dx(double time) {
_stepIfNecessary(time);
return currentSimulation.dx(time - currentIntervalOffset);
}
@override
bool isDone(double time) {
_stepIfNecessary(time);
return currentSimulation.isDone(time - currentIntervalOffset);
}
@override
set tolerance(Tolerance value) {
currentSimulation.tolerance = value;
super.tolerance = value;
}
}
...@@ -12,7 +12,7 @@ import 'package:flutter/physics.dart'; ...@@ -12,7 +12,7 @@ import 'package:flutter/physics.dart';
/// See also: /// See also:
/// ///
/// * [ClampingScrollSimulation], which implements Android scroll physics. /// * [ClampingScrollSimulation], which implements Android scroll physics.
class BouncingScrollSimulation extends SimulationGroup { class BouncingScrollSimulation extends Simulation {
/// Creates a simulation group for scrolling on iOS, with the given /// Creates a simulation group for scrolling on iOS, with the given
/// parameters. /// parameters.
/// ///
...@@ -31,68 +31,90 @@ class BouncingScrollSimulation extends SimulationGroup { ...@@ -31,68 +31,90 @@ class BouncingScrollSimulation extends SimulationGroup {
BouncingScrollSimulation({ BouncingScrollSimulation({
@required double position, @required double position,
@required double velocity, @required double velocity,
@required double leadingExtent, @required this.leadingExtent,
@required double trailingExtent, @required this.trailingExtent,
@required SpringDescription spring, @required this.spring,
Tolerance tolerance: Tolerance.defaultTolerance, Tolerance tolerance: Tolerance.defaultTolerance,
}) : _leadingExtent = leadingExtent, }) : super(tolerance: tolerance) {
_trailingExtent = trailingExtent,
_spring = spring,
super(tolerance: tolerance) {
assert(position != null); assert(position != null);
assert(velocity != null); assert(velocity != null);
assert(_leadingExtent != null); assert(leadingExtent != null);
assert(_trailingExtent != null); assert(trailingExtent != null);
assert(_leadingExtent <= _trailingExtent); assert(leadingExtent <= trailingExtent);
assert(_spring != null); assert(spring != null);
_chooseSimulation(position, velocity, 0.0);
if (position < leadingExtent) {
_springSimulation = _underscrollSimulation(position, velocity);
_springTime = double.NEGATIVE_INFINITY;
} else if (position > trailingExtent) {
_springSimulation = _overscrollSimulation(position, velocity);
_springTime = double.NEGATIVE_INFINITY;
} else {
_frictionSimulation = new FrictionSimulation(0.135, position, velocity);
final double finalX = _frictionSimulation.finalX;
if (velocity > 0.0 && finalX > trailingExtent) {
_springTime = _frictionSimulation.timeAtX(trailingExtent);
_springSimulation = _overscrollSimulation(trailingExtent, _frictionSimulation.dx(_springTime));
assert(_springTime.isFinite);
} else if (velocity < 0.0 && finalX < leadingExtent) {
_springTime = _frictionSimulation.timeAtX(leadingExtent);
_springSimulation = _underscrollSimulation(leadingExtent, _frictionSimulation.dx(_springTime));
assert(_springTime.isFinite);
} else {
_springTime = double.INFINITY;
}
}
assert(_springTime != null);
} }
final double _leadingExtent; /// When [x] falls below this value the simulation switches from an internal friction
final double _trailingExtent; /// model to a spring model which causes [x] to "spring" back to [leadingExtent].
final SpringDescription _spring; final double leadingExtent;
bool _isSpringing = false; /// When [x] exceeds this value the simulation switches from an internal friction
Simulation _currentSimulation; /// model to a spring model which causes [x] to "spring" back to [trailingExtent].
double _offset = 0.0; final double trailingExtent;
// This simulation can only step forward. /// The spring used used to return [x] to either [leadingExtent] or [trailingExtent].
@override final SpringDescription spring;
bool step(double time) => _chooseSimulation(
_currentSimulation.x(time - _offset),
_currentSimulation.dx(time - _offset),
time,
);
@override FrictionSimulation _frictionSimulation;
Simulation get currentSimulation => _currentSimulation; Simulation _springSimulation;
double _springTime;
double _timeOffset = 0.0;
@override Simulation _underscrollSimulation(double x, double dx) {
double get currentIntervalOffset => _offset; return new ScrollSpringSimulation(spring, x, leadingExtent, dx);
}
bool _chooseSimulation(double position, double velocity, double intervalOffset) { Simulation _overscrollSimulation(double x, double dx) {
if (!_isSpringing) { return new ScrollSpringSimulation(spring, x, trailingExtent, dx);
if (position > _trailingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _trailingExtent, velocity, tolerance: tolerance);
return true;
} else if (position < _leadingExtent) {
_isSpringing = true;
_offset = intervalOffset;
_currentSimulation = new ScrollSpringSimulation(_spring, position, _leadingExtent, velocity, tolerance: tolerance);
return true;
} else if (_currentSimulation == null) {
_currentSimulation = new FrictionSimulation(0.135, position, velocity, tolerance: tolerance);
return true;
} }
Simulation _simulation(double time) {
Simulation simulation;
if (time > _springTime) {
_timeOffset = _springTime.isFinite ? _springTime : 0.0;
simulation = _springSimulation;
} else {
_timeOffset = 0.0;
simulation = _frictionSimulation;
} }
return false; return simulation..tolerance = tolerance;
} }
@override
double x(double time) => _simulation(time).x(time - _timeOffset);
@override
double dx(double time) => _simulation(time).dx(time - _timeOffset);
@override
bool isDone(double time) => _simulation(time).isDone(time - _timeOffset);
@override @override
String toString() { String toString() {
return '$runtimeType(leadingExtent: $_leadingExtent, trailingExtent: $_trailingExtent)'; return '$runtimeType(leadingExtent: $leadingExtent, trailingExtent: $trailingExtent)';
} }
} }
......
// Copyright 2017 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 'package:flutter/physics.dart';
import 'package:flutter_test/flutter_test.dart';
const double _kEpsilon = .00001;
void main() {
test('Friction simulation positive velocity', () {
final FrictionSimulation friction = new FrictionSimulation(0.135, 100.0, 100.0);
expect(friction.x(0.0), closeTo(100.0, _kEpsilon));
expect(friction.dx(0.0), closeTo(100.0, _kEpsilon));
expect(friction.x(0.1), closeTo(110.0, 1.0));
expect(friction.x(0.5), closeTo(131.0, 1.0));
expect(friction.x(2.0), closeTo(149.0, 1.0));
expect(friction.finalX, closeTo(149.0, 1.0));
expect(friction.timeAtX(100.0), 0.0);
expect(friction.timeAtX(friction.x(0.1)), closeTo(0.1, _kEpsilon));
expect(friction.timeAtX(friction.x(0.5)), closeTo(0.5, _kEpsilon));
expect(friction.timeAtX(friction.x(2.0)), closeTo(2.0, _kEpsilon));
expect(friction.timeAtX(-1.0), double.INFINITY);
expect(friction.timeAtX(200.0), double.INFINITY);
});
test('Friction simulation negative velocity', () {
final FrictionSimulation friction = new FrictionSimulation(0.135, 100.0, -100.0);
expect(friction.x(0.0), closeTo(100.0, _kEpsilon));
expect(friction.dx(0.0), closeTo(-100.0, _kEpsilon));
expect(friction.x(0.1), closeTo(91.0, 1.0));
expect(friction.x(0.5), closeTo(68.0, 1.0));
expect(friction.x(2.0), closeTo(51.0, 1.0));
expect(friction.finalX, closeTo(50, 1.0));
expect(friction.timeAtX(100.0), 0.0);
expect(friction.timeAtX(friction.x(0.1)), closeTo(0.1, _kEpsilon));
expect(friction.timeAtX(friction.x(0.5)), closeTo(0.5, _kEpsilon));
expect(friction.timeAtX(friction.x(2.0)), closeTo(2.0, _kEpsilon));
expect(friction.timeAtX(101.0), double.INFINITY);
expect(friction.timeAtX(40.0), double.INFINITY);
});
}
...@@ -256,9 +256,6 @@ void main() { ...@@ -256,9 +256,6 @@ void main() {
expect(scroll.isDone(5.0), true); expect(scroll.isDone(5.0), true);
expect(scroll.x(5.0), closeTo(300.0, 1.0)); expect(scroll.x(5.0), closeTo(300.0, 1.0));
// We should never switch
expect(scroll.currentIntervalOffset, 0.0);
}); });
test('over/under scroll spring', () { test('over/under scroll spring', () {
...@@ -276,12 +273,30 @@ void main() { ...@@ -276,12 +273,30 @@ void main() {
expect(scroll.x(0.0), closeTo(500.0, .0001)); expect(scroll.x(0.0), closeTo(500.0, .0001));
expect(scroll.dx(0.0), closeTo(-7500.0, .0001)); expect(scroll.dx(0.0), closeTo(-7500.0, .0001));
expect(scroll.isDone(0.025), false); // Expect to reach 0.0 at about t=.07 at which point the simulation will
expect(scroll.x(0.025), closeTo(317.0, 1.0)); // switch from friction to the spring
expect(scroll.dx(0.25), closeTo(-4546, 1.0)); expect(scroll.isDone(0.065), false);
expect(scroll.x(0.065), closeTo(42.0, 1.0));
expect(scroll.dx(0.065), closeTo(-6584.0, 1.0));
// We've overscrolled (0.1 > 0.07). Trigger the underscroll
// simulation, and reverse direction
expect(scroll.isDone(0.1), false);
expect(scroll.x(0.1), closeTo(-123.0, 1.0));
expect(scroll.dx(0.1), closeTo(-2613.0, 1.0));
// Headed back towards 0.0 and slowing down.
expect(scroll.isDone(0.5), false);
expect(scroll.x(0.5), closeTo(-15.0, 1.0));
expect(scroll.dx(0.5), closeTo(124.0, 1.0));
// Now jump back to the beginning, because we can.
expect(scroll.isDone(0.0), false);
expect(scroll.x(0.0), closeTo(500.0, .0001));
expect(scroll.dx(0.0), closeTo(-7500.0, .0001));
expect(scroll.isDone(2.0), true); expect(scroll.isDone(2.0), true);
expect(scroll.x(2.0), 0.0); expect(scroll.x(2.0), 0.0);
expect(scroll.dx(2.0), closeTo(0.0, 45.0)); expect(scroll.dx(2.0), closeTo(0.0, 1.0));
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment