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';
export 'src/physics/friction_simulation.dart';
export 'src/physics/gravity_simulation.dart';
export 'src/physics/simulation.dart';
export 'src/physics/simulation_group.dart';
export 'src/physics/spring_simulation.dart';
export 'src/physics/tolerance.dart';
export 'src/physics/utils.dart';
......@@ -71,6 +71,20 @@ class FrictionSimulation extends Simulation {
@override
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
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';
/// See also:
///
/// * [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
/// parameters.
///
......@@ -31,68 +31,90 @@ class BouncingScrollSimulation extends SimulationGroup {
BouncingScrollSimulation({
@required double position,
@required double velocity,
@required double leadingExtent,
@required double trailingExtent,
@required SpringDescription spring,
@required this.leadingExtent,
@required this.trailingExtent,
@required this.spring,
Tolerance tolerance: Tolerance.defaultTolerance,
}) : _leadingExtent = leadingExtent,
_trailingExtent = trailingExtent,
_spring = spring,
super(tolerance: tolerance) {
}) : super(tolerance: tolerance) {
assert(position != null);
assert(velocity != null);
assert(_leadingExtent != null);
assert(_trailingExtent != null);
assert(_leadingExtent <= _trailingExtent);
assert(_spring != null);
_chooseSimulation(position, velocity, 0.0);
assert(leadingExtent != null);
assert(trailingExtent != null);
assert(leadingExtent <= trailingExtent);
assert(spring != null);
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;
final double _trailingExtent;
final SpringDescription _spring;
/// When [x] falls below this value the simulation switches from an internal friction
/// model to a spring model which causes [x] to "spring" back to [leadingExtent].
final double leadingExtent;
/// When [x] exceeds this value the simulation switches from an internal friction
/// model to a spring model which causes [x] to "spring" back to [trailingExtent].
final double trailingExtent;
bool _isSpringing = false;
Simulation _currentSimulation;
double _offset = 0.0;
/// The spring used used to return [x] to either [leadingExtent] or [trailingExtent].
final SpringDescription spring;
FrictionSimulation _frictionSimulation;
Simulation _springSimulation;
double _springTime;
double _timeOffset = 0.0;
Simulation _underscrollSimulation(double x, double dx) {
return new ScrollSpringSimulation(spring, x, leadingExtent, dx);
}
Simulation _overscrollSimulation(double x, double dx) {
return new ScrollSpringSimulation(spring, x, trailingExtent, dx);
}
Simulation _simulation(double time) {
Simulation simulation;
if (time > _springTime) {
_timeOffset = _springTime.isFinite ? _springTime : 0.0;
simulation = _springSimulation;
} else {
_timeOffset = 0.0;
simulation = _frictionSimulation;
}
return simulation..tolerance = tolerance;
}
// This simulation can only step forward.
@override
bool step(double time) => _chooseSimulation(
_currentSimulation.x(time - _offset),
_currentSimulation.dx(time - _offset),
time,
);
double x(double time) => _simulation(time).x(time - _timeOffset);
@override
Simulation get currentSimulation => _currentSimulation;
double dx(double time) => _simulation(time).dx(time - _timeOffset);
@override
double get currentIntervalOffset => _offset;
bool _chooseSimulation(double position, double velocity, double intervalOffset) {
if (!_isSpringing) {
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;
}
}
return false;
}
bool isDone(double time) => _simulation(time).isDone(time - _timeOffset);
@override
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() {
expect(scroll.isDone(5.0), true);
expect(scroll.x(5.0), closeTo(300.0, 1.0));
// We should never switch
expect(scroll.currentIntervalOffset, 0.0);
});
test('over/under scroll spring', () {
......@@ -276,12 +273,30 @@ void main() {
expect(scroll.x(0.0), closeTo(500.0, .0001));
expect(scroll.dx(0.0), closeTo(-7500.0, .0001));
expect(scroll.isDone(0.025), false);
expect(scroll.x(0.025), closeTo(317.0, 1.0));
expect(scroll.dx(0.25), closeTo(-4546, 1.0));
// Expect to reach 0.0 at about t=.07 at which point the simulation will
// switch from friction to the spring
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.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