| Index: client/touch/Momentum.dart
|
| ===================================================================
|
| --- client/touch/Momentum.dart (revision 4144)
|
| +++ client/touch/Momentum.dart (working copy)
|
| @@ -1,554 +0,0 @@
|
| -// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
| -// for details. All rights reserved. Use of this source code is governed by a
|
| -// BSD-style license that can be found in the LICENSE file.
|
| -
|
| -/**
|
| - * Implementations can be used to simulate the deceleration of an element within
|
| - * a certain region. To use this behavior you need to provide an initial
|
| - * velocity that is meant to represent the gesture that is initiating this
|
| - * deceleration. You also provide the bounds of the region that the element
|
| - * exists in, and the current offset of the element within that region. The
|
| - * transitions will have the element decelerate to rest, or stretch past the
|
| - * offset boundaries and then come to rest.
|
| - *
|
| - * This is primarily designed to solve the problem of slow scrolling in mobile
|
| - * safari. You can use this along with the [Scroller] behavior to make a
|
| - * scrollable area scroll the same way it would in a native application.
|
| - *
|
| - * Implementations of this interface do not maintain any references to HTML
|
| - * elements, and therefore cannot do any redrawing of elements. They only
|
| - * calculates where the element should be on an interval. It is the delegate's
|
| - * responsibility to redraw the element when the onDecelerate callback is
|
| - * invoked. It is recommended that you move the element with a hardware
|
| - * accelerated method such as using 'translate3d' on the element's
|
| - * -webkit-transform style property.
|
| - */
|
| -interface Momentum default TimeoutMomentum {
|
| -
|
| - Momentum(MomentumDelegate delegate, [num defaultDecelerationFactor]);
|
| -
|
| - bool get decelerating();
|
| -
|
| - num get decelerationFactor();
|
| -
|
| - /**
|
| - * Transition end handler. This function must be invoked after any transition
|
| - * that occurred as a result of a call to the delegate's onDecelerate callback.
|
| - */
|
| - void onTransitionEnd();
|
| -
|
| - /**
|
| - * Start decelerating.
|
| - * The [velocity] passed should be in terms of number of pixels / millisecond.
|
| - * [minCoord] and [maxCoord] specify the content's scrollable boundary.
|
| - * The current offset of the element within its boundaries is specified by
|
| - * [initialOffset].
|
| - * Returns true if deceleration has been initiated.
|
| - */
|
| - bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
|
| - Coordinate initialOffset, [num decelerationFactor]);
|
| -
|
| - /**
|
| - * Calculate the velocity required to transition between coordinates [start]
|
| - * and [target] optionally specifying a custom [decelerationFactor].
|
| - */
|
| - Coordinate calculateVelocity(Coordinate start, Coordinate target,
|
| - [num decelerationFactor]);
|
| -
|
| - /** Stop decelerating and return the current velocity. */
|
| - Coordinate stop();
|
| -
|
| - /** Aborts decelerating without dispatching any notification events. */
|
| - void abort();
|
| -
|
| - /** null if no transition is in progress. */
|
| - Coordinate get destination();
|
| -}
|
| -
|
| -/**
|
| - * Momentum Delegate interface.
|
| - * You are required to implement this interface in order to use the
|
| - * Momentum behavior.
|
| - */
|
| -interface MomentumDelegate {
|
| - /**
|
| - * Callback for a deceleration step. The delegate is responsible for redrawing
|
| - * the element in its new position specified in px.
|
| - */
|
| - void onDecelerate(num x, num y);
|
| -
|
| - /**
|
| - * Callback for end of deceleration.
|
| - */
|
| - void onDecelerationEnd();
|
| -}
|
| -
|
| -class BouncingState {
|
| - static final NOT_BOUNCING = 0;
|
| - static final BOUNCING_AWAY = 1;
|
| - static final BOUNCING_BACK = 2;
|
| -}
|
| -
|
| -class _Move {
|
| - final num x;
|
| - final num y;
|
| - final num vx;
|
| - final num vy;
|
| - final num time;
|
| -
|
| - _Move(this.x, this.y, this.vx, this.vy, this.time);
|
| -}
|
| -
|
| -/**
|
| - * Secant method root solver helper class.
|
| - * We use http://en.wikipedia.org/wiki/Secant_method
|
| - * falling back to the http://en.wikipedia.org/wiki/Bisection_method
|
| - * if it doesn't appear we are converging properlty.
|
| - * TODO(jacobr): simplify the code so we don't have to use this solver
|
| - * class at all.
|
| - */
|
| -class Solver {
|
| -
|
| - static num solve(num fn(num), num targetY, num startX,
|
| - [int maxIterations = 50]) {
|
| - num lastX = 0;
|
| - num lastY = fn(lastX);
|
| - num deltaX;
|
| - num deltaY;
|
| - num minX = null;
|
| - num maxX = null;
|
| - num x = startX;
|
| - num delta = startX;
|
| - for (int i = 0; i < maxIterations; i++) {
|
| - num y = fn(x);
|
| - if (y.round() == targetY.round()) {
|
| - return x;
|
| - }
|
| - if (y > targetY) {
|
| - maxX = x;
|
| - } else {
|
| - minX = x;
|
| - }
|
| -
|
| - num errorY = targetY - y;
|
| - deltaX = x - lastX;
|
| - deltaY = y - lastY;
|
| - lastX = x;
|
| - lastY = y;
|
| - // Avoid divide by zero and as a hack just repeat the previous delta.
|
| - // Obviously this is a little dangerous and we might not converge.
|
| - if (deltaY != 0) {
|
| - delta = errorY * deltaX / deltaY;
|
| - }
|
| - x += delta;
|
| - if (minX != null && maxX != null
|
| - && (x > minX || x < maxX)) {
|
| - // Fall back to binary search.
|
| - x = (minX + maxX) / 2;
|
| - }
|
| - }
|
| - window.console.warn('''Could not find an exact solution. LastY=${lastY},
|
| - targetY=${targetY} lastX=$lastX delta=$delta deltaX=$deltaX
|
| - deltaY=$deltaY''');
|
| - return x;
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Helper class modeling the physics of a throwable scrollable area along a
|
| - * single dimension.
|
| - */
|
| -class SingleDimensionPhysics {
|
| - /** The number of frames per second the animation should run at. */
|
| - static final _FRAMES_PER_SECOND = 60;
|
| -
|
| - /**
|
| - * The spring coefficient for when the element has passed a boundary and is
|
| - * decelerating to change direction and bounce back. Each frame, the velocity
|
| - * will be changed by x times this coefficient, where x is the current stretch
|
| - * value of the element from its boundary. This will end when velocity reaches
|
| - * zero.
|
| - */
|
| - static final _PRE_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
|
| -
|
| - /**
|
| - * The spring coefficient for when the element is bouncing back from a
|
| - * stretched offset to a min or max position. Each frame, the velocity will
|
| - * be changed to x times this coefficient, where x is the current stretch
|
| - * value of the element from its boundary. This will end when the stretch
|
| - * value reaches 0.
|
| - */
|
| - static final _POST_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
|
| -
|
| - /**
|
| - * The number of milliseconds per animation frame.
|
| - */
|
| - static final _MS_PER_FRAME = 1000.0 / _FRAMES_PER_SECOND;
|
| -
|
| - /**
|
| - * The constant factor applied to velocity at each frame to simulate
|
| - * deceleration.
|
| - */
|
| - static final _DECELERATION_FACTOR = 0.97;
|
| -
|
| -
|
| - static final _MAX_VELOCITY_STATIC_FRICTION = 0.08 * _MS_PER_FRAME;
|
| - static final _DECELERATION_FACTOR_STATIC_FRICTION = 0.92;
|
| -
|
| - /**
|
| - * Minimum velocity required to start or continue deceleration, in
|
| - * pixels/frame. This is equivalent to 0.25 px/ms.
|
| - */
|
| - static final _MIN_VELOCITY = 0.25 * _MS_PER_FRAME;
|
| -
|
| - /**
|
| - * Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
|
| - * px/ms.
|
| - */
|
| - static final _MIN_STEP_VELOCITY = 0.01 * _MS_PER_FRAME;
|
| -
|
| - /**
|
| - * Boost the initial velocity by a certain factor before applying momentum.
|
| - * This just gives the momentum a better feel.
|
| - */
|
| - static final _INITIAL_VELOCITY_BOOST_FACTOR = 1.25;
|
| -
|
| - /**
|
| - * Additional deceleration factor to apply for the current move only. This
|
| - * is helpful for cases such as scroll wheel scrolling where the default
|
| - * amount of deceleration is inadequate.
|
| - */
|
| - num customDecelerationFactor = 1;
|
| - num _minCoord;
|
| - num _maxCoord;
|
| -
|
| - /** The bouncing state. */
|
| - int _bouncingState;
|
| -
|
| -
|
| - num velocity;
|
| - num _currentOffset;
|
| -
|
| - /**
|
| - * constant used when guessing at the velocity required to throw to a specific
|
| - * location. Chosen arbitrarily. All that really matters is that the velocity
|
| - * is large enough that a throw gesture will occur.
|
| - */
|
| - static final _VELOCITY_GUESS = 20;
|
| -
|
| - SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING {
|
| - }
|
| -
|
| - void configure(num minCoord, num maxCoord,
|
| - num initialOffset, num customDecelerationFactor_,
|
| - num velocity_) {
|
| - _bouncingState = BouncingState.NOT_BOUNCING;
|
| - _minCoord = minCoord;
|
| - _maxCoord = maxCoord;
|
| - _currentOffset = initialOffset;
|
| - this.customDecelerationFactor = customDecelerationFactor_;
|
| - _adjustInitialVelocityAndBouncingState(velocity_);
|
| - }
|
| -
|
| - num solve(num initialOffset, num targetOffset,
|
| - num customDecelerationFactor_) {
|
| - initialOffset = initialOffset.round();
|
| - targetOffset = targetOffset.round();
|
| - if (initialOffset == targetOffset) {
|
| - return 0;
|
| - }
|
| - return Solver.solve((num velocity_) {
|
| - // Don't specify min and max coordinates as we don't need to bother
|
| - // with the simulating bouncing off the edges.
|
| - configure(null, null, initialOffset.round(),
|
| - customDecelerationFactor_, velocity_);
|
| - stepAll();
|
| - return _currentOffset;
|
| - },
|
| - targetOffset,
|
| - targetOffset > initialOffset ? _VELOCITY_GUESS : -_VELOCITY_GUESS);
|
| - }
|
| -
|
| - /**
|
| - * Helper method to calculate initial velocity.
|
| - * The [velocity] passed here should be in terms of number of
|
| - * pixels / millisecond. Returns the adjusted x and y velocities.
|
| - */
|
| - void _adjustInitialVelocityAndBouncingState(num v) {
|
| - velocity = v * _MS_PER_FRAME * _INITIAL_VELOCITY_BOOST_FACTOR;
|
| -
|
| - if (velocity.abs() < _MIN_VELOCITY) {
|
| - if (_minCoord !== null && _currentOffset < _minCoord) {
|
| - velocity = (_minCoord - _currentOffset) * _POST_BOUNCE_COEFFICIENT;
|
| - velocity = Math.max(velocity, _MIN_STEP_VELOCITY);
|
| - _bouncingState = BouncingState.BOUNCING_BACK;
|
| - } else if (_maxCoord !== null && _currentOffset > _maxCoord) {
|
| - velocity = (_currentOffset - _maxCoord) * _POST_BOUNCE_COEFFICIENT;
|
| - velocity = -Math.max(velocity, _MIN_STEP_VELOCITY);
|
| - _bouncingState = BouncingState.BOUNCING_BACK;
|
| - }
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Apply deceleration.
|
| - */
|
| - void _adjustVelocity() {
|
| - num speed = velocity.abs();
|
| - velocity *= _DECELERATION_FACTOR;
|
| - if (customDecelerationFactor != null) {
|
| - velocity *= customDecelerationFactor;
|
| - }
|
| - // This isn't really how static friction works but it is a plausible
|
| - // approximation.
|
| - if (speed < _MAX_VELOCITY_STATIC_FRICTION) {
|
| - velocity *= _DECELERATION_FACTOR_STATIC_FRICTION;
|
| - }
|
| -
|
| - num stretchDistance;
|
| - if (_minCoord !== null && _currentOffset < _minCoord) {
|
| - stretchDistance = _minCoord - _currentOffset;
|
| - } else {
|
| - if (_maxCoord !== null && _currentOffset > _maxCoord) {
|
| - stretchDistance = _maxCoord - _currentOffset;
|
| - }
|
| - }
|
| - if (stretchDistance != null) {
|
| - if (stretchDistance * velocity < 0) {
|
| - _bouncingState = _bouncingState == BouncingState.BOUNCING_BACK ?
|
| - BouncingState.NOT_BOUNCING : BouncingState.BOUNCING_AWAY;
|
| - velocity += stretchDistance * _PRE_BOUNCE_COEFFICIENT;
|
| - } else {
|
| - _bouncingState = BouncingState.BOUNCING_BACK;
|
| - velocity = stretchDistance > 0 ?
|
| - Math.max(stretchDistance * _POST_BOUNCE_COEFFICIENT,
|
| - _MIN_STEP_VELOCITY) :
|
| - Math.min(stretchDistance * _POST_BOUNCE_COEFFICIENT,
|
| - -_MIN_STEP_VELOCITY);
|
| - }
|
| - } else {
|
| - _bouncingState = BouncingState.NOT_BOUNCING;
|
| - }
|
| - }
|
| -
|
| - void step() {
|
| - // It is common for scrolling to be disabled so in these cases we want to
|
| - // avoid needless calculations.
|
| - if (velocity !== null) {
|
| - _currentOffset += velocity;
|
| - _adjustVelocity();
|
| - }
|
| - }
|
| -
|
| - void stepAll() {
|
| - while(!isDone()) {
|
| - step();
|
| - }
|
| - }
|
| -
|
| - /**
|
| - * Whether or not the current velocity is above the threshold required to
|
| - * continue decelerating.
|
| - */
|
| - bool isVelocityAboveThreshold(num threshold) {
|
| - return velocity.abs() >= threshold;
|
| - }
|
| -
|
| - bool isDone() {
|
| - return _bouncingState == BouncingState.NOT_BOUNCING &&
|
| - !isVelocityAboveThreshold(_MIN_STEP_VELOCITY);
|
| - }
|
| -}
|
| -
|
| -/**
|
| - * Implementation of a momentum strategy using webkit-transforms
|
| - * and timeouts.
|
| - */
|
| -class TimeoutMomentum implements Momentum {
|
| -
|
| - SingleDimensionPhysics physicsX;
|
| - SingleDimensionPhysics physicsY;
|
| - Coordinate _previousOffset;
|
| - Queue<_Move> _moves;
|
| - num _stepTimeout;
|
| - bool _decelerating;
|
| - MomentumDelegate _delegate;
|
| - int _nextY;
|
| - int _nextX;
|
| - Coordinate _minCoord;
|
| - Coordinate _maxCoord;
|
| - num _customDecelerationFactor;
|
| - num _defaultDecelerationFactor;
|
| -
|
| - TimeoutMomentum(this._delegate, [num defaultDecelerationFactor = 1])
|
| - : _defaultDecelerationFactor = defaultDecelerationFactor,
|
| - _decelerating = false,
|
| - _moves = new Queue<_Move>(),
|
| - physicsX = new SingleDimensionPhysics(),
|
| - physicsY = new SingleDimensionPhysics();
|
| -
|
| - /**
|
| - * Calculate and return the moves for the deceleration motion.
|
| - */
|
| - void _calculateMoves() {
|
| - _moves.clear();
|
| - num time = TimeUtil.now();
|
| - while (!physicsX.isDone() || !physicsY.isDone()) {
|
| - _stepWithoutAnimation();
|
| - time += SingleDimensionPhysics._MS_PER_FRAME;
|
| - if (_isStepNecessary()) {
|
| - _moves.add(new _Move(_nextX, _nextY,
|
| - physicsX.velocity,
|
| - physicsY.velocity, time));
|
| - _previousOffset.y = _nextY;
|
| - _previousOffset.x = _nextX;
|
| - }
|
| - }
|
| - }
|
| -
|
| - bool get decelerating() => _decelerating;
|
| - num get decelerationFactor() => _customDecelerationFactor;
|
| -
|
| - /**
|
| - * Checks whether or not an animation step is necessary or not. Animations
|
| - * steps are not necessary when the velocity gets so low that in several
|
| - * frames the offset is the same.
|
| - * Returns true if there is movement to be done in the next frame.
|
| - */
|
| - bool _isStepNecessary() {
|
| - return _nextY != _previousOffset.y || _nextX != _previousOffset.x;
|
| - }
|
| -
|
| - /**
|
| - * The [TouchHandler] requires this function but we don't need to do
|
| - * anything here.
|
| - */
|
| - void onTransitionEnd() {
|
| - }
|
| -
|
| - Coordinate calculateVelocity(Coordinate start_, Coordinate target,
|
| - [num decelerationFactor = null]) {
|
| - return new Coordinate(
|
| - physicsX.solve(start_.x, target.x, decelerationFactor),
|
| - physicsY.solve(start_.y, target.y, decelerationFactor));
|
| - }
|
| -
|
| - bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
|
| - Coordinate initialOffset, [num decelerationFactor = null]) {
|
| - _customDecelerationFactor = _defaultDecelerationFactor;
|
| - if (decelerationFactor !== null) {
|
| - _customDecelerationFactor = decelerationFactor;
|
| - }
|
| -
|
| - if (_stepTimeout !== null) {
|
| - Env.cancelRequestAnimationFrame(_stepTimeout);
|
| - _stepTimeout = null;
|
| - }
|
| -
|
| - assert (_stepTimeout === null);
|
| - assert(minCoord.x <= maxCoord.x);
|
| - assert(minCoord.y <= maxCoord.y);
|
| - _previousOffset = initialOffset.clone();
|
| - physicsX.configure(minCoord.x, maxCoord.x, initialOffset.x,
|
| - _customDecelerationFactor, velocity.x);
|
| - physicsY.configure(minCoord.y, maxCoord.y, initialOffset.y,
|
| - _customDecelerationFactor, velocity.y);
|
| - if (!physicsX.isDone() || !physicsY.isDone()) {
|
| - _calculateMoves();
|
| - if (!_moves.isEmpty()) {
|
| - num firstTime = _moves.first().time;
|
| - _stepTimeout = Env.requestAnimationFrame(
|
| - _step, null, firstTime);
|
| - _decelerating = true;
|
| - return true;
|
| - }
|
| - }
|
| - _decelerating = false;
|
| - return false;
|
| - }
|
| -
|
| - /**
|
| - * Update the x, y values of the element offset without actually moving the
|
| - * element. This is done because we store decimal values for x, y for
|
| - * precision, but moving is only required when the offset is changed by at
|
| - * least a whole integer.
|
| - */
|
| - void _stepWithoutAnimation() {
|
| - physicsX.step();
|
| - physicsY.step();
|
| - // TODO(jacobr): double.round() should return an int, see b/5121907
|
| - _nextX = physicsX._currentOffset.round().toInt();
|
| - _nextY = physicsY._currentOffset.round().toInt();
|
| - }
|
| -
|
| - /**
|
| - * Calculate the next offset of the element and animate it to that position.
|
| - */
|
| - void _step(num timestamp) {
|
| - _stepTimeout = null;
|
| -
|
| - // Prune moves that are more than 1 frame behind when we have more
|
| - // available moves.
|
| - num lastEpoch = timestamp - SingleDimensionPhysics._MS_PER_FRAME;
|
| - while (!_moves.isEmpty() && _moves.first() !== _moves.last()
|
| - && _moves.first().time < lastEpoch) {
|
| - _moves.removeFirst();
|
| - }
|
| -
|
| - if (!_moves.isEmpty()) {
|
| - final move = _moves.removeFirst();
|
| - _delegate.onDecelerate(move.x, move.y);
|
| - if (!_moves.isEmpty()) {
|
| - num nextTime = _moves.first().time;
|
| - assert(_stepTimeout === null);
|
| - _stepTimeout = Env.requestAnimationFrame(_step, null, nextTime);
|
| - } else {
|
| - stop();
|
| - }
|
| - }
|
| - }
|
| -
|
| - void abort() {
|
| - _decelerating = false;
|
| - _moves.clear();
|
| - if (_stepTimeout !== null) {
|
| - Env.cancelRequestAnimationFrame(_stepTimeout);
|
| - _stepTimeout = null;
|
| - }
|
| - }
|
| -
|
| - Coordinate stop() {
|
| - final wasDecelerating = _decelerating;
|
| - _decelerating = false;
|
| - Coordinate velocity;
|
| - if (!_moves.isEmpty()) {
|
| - final move = _moves.first();
|
| - // This is a workaround for the ugly hacks that get applied when a user
|
| - // passed a velocity in to this Momentum implementation.
|
| - num velocityScale = SingleDimensionPhysics._MS_PER_FRAME *
|
| - SingleDimensionPhysics._INITIAL_VELOCITY_BOOST_FACTOR;
|
| - velocity = new Coordinate(
|
| - move.vx / velocityScale, move.vy / velocityScale);
|
| - } else {
|
| - velocity = new Coordinate(0, 0);
|
| - }
|
| - _moves.clear();
|
| - if (_stepTimeout !== null) {
|
| - Env.cancelRequestAnimationFrame(_stepTimeout);
|
| - _stepTimeout = null;
|
| - }
|
| - if (wasDecelerating) {
|
| - _delegate.onDecelerationEnd();
|
| - }
|
| - return velocity;
|
| - }
|
| -
|
| - Coordinate get destination() {
|
| - if (!_moves.isEmpty()) {
|
| - final lastMove = _moves.last();
|
| - return new Coordinate(lastMove.x, lastMove.y);
|
| - } else {
|
| - return null;
|
| - }
|
| - }
|
| -}
|
|
|