| /* |
| * Copyright 2010 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.mobile.client; |
| |
| import com.google.gwt.core.client.Duration; |
| import com.google.gwt.user.client.Timer; |
| |
| /** |
| * This class can be used to simulate the deceleration of an element within a |
| * certain region. To use this behavior you need to provide a distance and time |
| * that is meant to represent a 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. This behavior will step |
| * through all of the intermediate points necessary to decelerate the element |
| * back to a velocity of zero. In doing so, the element may 'bounce' in and out |
| * of the boundaries of the region, but will always come to rest within the |
| * region. |
| * |
| * This is primarily designed to solve the problem of slow scrolling in mobile |
| * safari. You can use this along with the Scroller behavior |
| * (wireless.fx.Scroller) to make a scrollable area scroll as well as it would |
| * in a native application. |
| * |
| * This class does not maintain any references to HTML elements, and therefore |
| * cannot do any redrawing of elements. It 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. |
| */ |
| class Momentum { |
| |
| /** |
| * You are required to implement this interface in order to use the |
| * {@link Momentum} behavior. |
| */ |
| public interface Delegate { |
| |
| /** |
| * Callback for a deceleration step. The delegate is responsible for redrawing |
| * the element in its new position. |
| * |
| * @param floorX The new x offset |
| * @param floorY The new y offset |
| * @param velocity The current velocitiy |
| */ |
| void onDecelerate(double floorX, double floorY, Point velocity); |
| |
| /** |
| * Callback for end of deceleration. |
| */ |
| void onDecelerationEnd(); |
| } |
| |
| /** |
| * The constant factor applied to velocity at each frame to simulate |
| * deceleration. |
| */ |
| private static final double DECELERATION_FACTOR = 0.98; |
| |
| /** |
| * The velocity threshold at which declereration will end. |
| */ |
| private static final double DECELERATION_STOP_VELOCITY = 0.01; |
| |
| /** |
| * The number of frames per second the animation should run at. |
| */ |
| private static final double FRAMES_PER_SECOND = 60; |
| |
| /** |
| * Boost the initial velocity by a certain factor before applying momentum. |
| * This just gives the momentum a better feel. |
| */ |
| private static final double INITIAL_VELOCITY_BOOST_FACTOR = 1.5; |
| |
| /** |
| * Minimum velocity required to start deceleration. |
| */ |
| private static final double MIN_START_VELOCITY = 0.3; |
| |
| /** |
| * The number of milliseconds per animation frame. |
| */ |
| private static final double MS_PER_FRAME = 1000 / 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. |
| */ |
| private static final double POST_BOUNCE_COEFFICIENT = 9 / FRAMES_PER_SECOND; |
| |
| /** |
| * 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. |
| */ |
| private static final double PRE_BOUNCE_COEFFICIENT = 1.8 / FRAMES_PER_SECOND; |
| |
| /** |
| * True when the momentum of has carried the position outside the allowable |
| * range but before the velocity has changed directions. |
| */ |
| private boolean bouncingX; |
| |
| /** |
| * True when the momentum of has carried the position outside the allowable |
| * range but before the velocity has changed directions. |
| */ |
| private boolean bouncingY; |
| |
| /** |
| * The current offset of the element. These x, y values can be decimal values. |
| * It is necessary to store these values for the sake of precision. |
| */ |
| private Point currentOffset; |
| |
| /** |
| * Whether or not deceleration is currently in progress. |
| */ |
| private boolean decelerating; |
| |
| private Delegate delegate; |
| |
| /** |
| * The maximum boundary for the element. |
| */ |
| private Point maxCoord; |
| |
| /** |
| * The minimum boundary for the element. |
| */ |
| private Point minCoord; |
| |
| /** |
| * The previous offset of the element. These x, y values are whole numbers. |
| * Their values are derived from rounding of the currentOffset_ member. |
| */ |
| private Point previousOffset; |
| |
| /** |
| * The start time of the deceleration. |
| */ |
| private double startTime; |
| |
| private Timer stepFunction = new Timer() { |
| @Override |
| public void run() { |
| step(); |
| } |
| }; |
| |
| /** |
| * The step number of the deceleration. |
| */ |
| private double stepNumber; |
| |
| /** |
| * Current velocity of the element. In this class velocity is measured as |
| * pixels per frame. That is, the number of pixels to move the element in the |
| * next frame. |
| */ |
| private Point velocity; |
| |
| /** |
| * Creates a new Momentum object. |
| * |
| * @param delegate The momentum delegate. |
| */ |
| public Momentum(Delegate delegate) { |
| this.delegate = delegate; |
| } |
| |
| /** |
| * Whether or not the element is currently bouncing. Bouncing is the behavior |
| * of an element moving past an allowable boundary and decelerating to change |
| * direction and snap back into place. Not to be confused with bouncing back. |
| * |
| * @return True if the element is currently bouncing in either the x or y |
| * direction. |
| */ |
| public boolean isBouncing() { |
| return bouncingY || bouncingX; |
| } |
| |
| /** |
| * Start decelerating. Checks if the current velocity is above the minumum |
| * threshold to start decelerating. If so then deceleration will begin, if not |
| * then nothing happens. |
| * |
| * @param velocity The initial velocity. The velocity passed here should be in |
| * terms of number of pixels / millisecond. initiating deceleration. |
| * @param minCoord The content's scrollable boundary. |
| * @param maxCoord The content's scrollable boundary. |
| * @param initialOffset The current offset of the element within its |
| * boundaries. |
| * @return True if deceleration has been initiated. |
| */ |
| public boolean start(Point velocity, Point minCoord, |
| Point maxCoord, Point initialOffset) { |
| this.minCoord = minCoord; |
| this.maxCoord = maxCoord; |
| |
| currentOffset = new Point(initialOffset); |
| previousOffset = new Point(initialOffset); |
| this.velocity = adjustInitialVelocity(velocity); |
| |
| if (isVelocityAboveThreshold(MIN_START_VELOCITY)) { |
| decelerating = true; |
| startTime = Duration.currentTimeMillis(); |
| stepNumber = 0; |
| stepFunction.schedule((int) MS_PER_FRAME); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Stop decelerating. |
| */ |
| public void stop() { |
| decelerating = false; |
| bouncingX = false; |
| bouncingY = false; |
| |
| delegate.onDecelerationEnd(); |
| } |
| |
| /** |
| * Helper method to calculate initial velocity. |
| * |
| * @param velocity The initial velocity. The velocity passed here should be in |
| * terms of number of pixels / millisecond. |
| * @return The adjusted x and y velocities. |
| */ |
| private Point adjustInitialVelocity(Point velocity) { |
| return new Point(adjustInitialVelocityForDirection(velocity.x, |
| currentOffset.x, minCoord.x, maxCoord.x), |
| adjustInitialVelocityForDirection(velocity.y, currentOffset.y, |
| minCoord.y, maxCoord.y)); |
| } |
| |
| /** |
| * Helper method to calculate the initial velocity for a specific direction. |
| * |
| * @param originalVelocity The velocity we are adjusting. |
| * @param offset The offset for this direction. |
| * @param min The min coordinate for this direction. |
| * @param max The max coordinate for this direction. |
| * @return The calculated velocity. |
| */ |
| private double adjustInitialVelocityForDirection(double originalVelocity, |
| double offset, double min, double max) { |
| // Convert from pixels/ms to pixels/frame |
| double vel = originalVelocity * MS_PER_FRAME |
| * INITIAL_VELOCITY_BOOST_FACTOR; |
| |
| // If the initial velocity is below the minimum threshold, it is possible |
| // that we need to bounce back depending on where the element is. |
| if (Math.abs(vel) < MIN_START_VELOCITY) { |
| // If either of these cases are true, then the element is outside of its |
| // allowable region and we need to apply a bounce back acceleration to |
| // bring it back to rest in its defined area. |
| if (offset < min) { |
| vel = (min - offset) * POST_BOUNCE_COEFFICIENT; |
| vel = Math.max(vel, MIN_START_VELOCITY); |
| } else if (offset > max) { |
| vel = (offset - max) * POST_BOUNCE_COEFFICIENT; |
| vel = -Math.max(vel, MIN_START_VELOCITY); |
| } |
| } |
| |
| return vel; |
| } |
| |
| /** |
| * Decelerate the current velocity. |
| */ |
| private void adjustVelocity() { |
| adjustVelocityComponent(currentOffset.x, minCoord.x, maxCoord.x, |
| velocity.x, bouncingX, false /* horizontal */ |
| ); |
| adjustVelocityComponent(currentOffset.y, minCoord.y, maxCoord.y, |
| velocity.y, bouncingY, true /* vertical */ |
| ); |
| } |
| |
| /** |
| * Apply deceleration to a specifc direction. |
| * |
| * @param offset The offset for this direction. |
| * @param min The min coordinate for this direction. |
| * @param max The max coordinate for this direction. |
| * @param velocity The velocity for this direction. |
| * @param bouncing Whether this direction is bouncing. |
| * @param vertical Whether or not the direction is vertical. |
| */ |
| private void adjustVelocityComponent(double offset, double min, double max, |
| double velocity, boolean bouncing, boolean vertical) { |
| double speed = Math.abs(velocity); |
| |
| // Apply the deceleration factor several times as we get closer to stopping. |
| int numTimes = speed < 15 ? (speed < 3 ? 3 : 2) : 1; |
| velocity *= Math.pow(DECELERATION_FACTOR, numTimes); |
| |
| double stretchDistance = 0; |
| |
| // We make special adjustments to the velocity if the element is outside of |
| // its region. |
| if (offset < min) { |
| stretchDistance = min - offset; |
| } else if (offset > max) { |
| stretchDistance = max - offset; |
| } |
| |
| // If stretchDistance has a value then we are either bouncing or bouncing |
| // back. |
| if (stretchDistance != 0) { |
| // If our adjustment is in the opposite direction of our velocity then we |
| // are still trying to turn around. Else we are bouncing back. |
| if (stretchDistance * velocity < 0) { |
| bouncing = true; |
| velocity += stretchDistance * PRE_BOUNCE_COEFFICIENT; |
| } else { |
| bouncing = false; |
| velocity = stretchDistance * POST_BOUNCE_COEFFICIENT; |
| } |
| } |
| |
| if (vertical) { |
| this.velocity.y = velocity; |
| bouncingY = bouncing; |
| } else { |
| this.velocity.x = velocity; |
| bouncingX = bouncing; |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @return True if there is movement to be done in the next frame. |
| */ |
| private boolean isStepNecessary() { |
| return Math.abs(currentOffset.y + velocity.y - previousOffset.y) > 1 |
| || Math.abs(currentOffset.x + velocity.x - previousOffset.x) > 1; |
| } |
| |
| /** |
| * Whether or not the current velocity is above the threshold required to |
| * continue decelerating. Once both the x and y velocity fall below the |
| * threshold, the element should stop moving entirely. |
| * |
| * @param threshold The threshold to measure against. |
| * @return True if the x or y velocity is still above the threshold. |
| */ |
| private boolean isVelocityAboveThreshold(double threshold) { |
| return Math.abs(velocity.x) >= threshold |
| || Math.abs(velocity.y) >= threshold; |
| } |
| |
| /** |
| * Calculate the next offset of the element and animate it to that position. |
| */ |
| private void step() { |
| // If deceleration is stopped between frames this is possible. Need to abort |
| // the step if this happens. |
| if (!decelerating) { |
| return; |
| } |
| |
| double now = Duration.currentTimeMillis(); |
| double framesExpected = Math.floor((now - startTime) / MS_PER_FRAME); |
| |
| // Do at least one step, and more if subsequent steps are not necessary or |
| // if we are falling behind. |
| do { |
| stepWithoutAnimation(); |
| } while (isVelocityAboveThreshold(DECELERATION_STOP_VELOCITY) |
| && (!isStepNecessary() || framesExpected > stepNumber)); |
| |
| double floorY = currentOffset.y; |
| double floorX = currentOffset.x; |
| |
| // If we have moved a whole integer then notify the delegate and update the |
| // previous position. |
| if (decelerating) { |
| delegate.onDecelerate(floorX, floorY, velocity); |
| previousOffset.y = floorY; |
| previousOffset.x = floorX; |
| } |
| |
| // This condition checks of deceleration is over. |
| if (!isBouncing() |
| && !isVelocityAboveThreshold(DECELERATION_STOP_VELOCITY)) { |
| stop(); |
| return; |
| } |
| |
| stepFunction.schedule((int) (MS_PER_FRAME * (1 + stepNumber - framesExpected))); |
| } |
| |
| /** |
| * 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. |
| */ |
| private void stepWithoutAnimation() { |
| stepNumber++; |
| currentOffset.y += velocity.y; |
| currentOffset.x += velocity.x; |
| |
| adjustVelocity(); |
| } |
| } |