1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/Axis.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,417 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import java.util.HashMap; 1.12 +import java.util.Map; 1.13 + 1.14 +import org.mozilla.gecko.GeckoAppShell; 1.15 +import org.mozilla.gecko.PrefsHelper; 1.16 +import org.mozilla.gecko.util.FloatUtils; 1.17 + 1.18 +import android.util.Log; 1.19 +import android.view.View; 1.20 + 1.21 +/** 1.22 + * This class represents the physics for one axis of movement (i.e. either 1.23 + * horizontal or vertical). It tracks the different properties of movement 1.24 + * like displacement, velocity, viewport dimensions, etc. pertaining to 1.25 + * a particular axis. 1.26 + */ 1.27 +abstract class Axis { 1.28 + private static final String LOGTAG = "GeckoAxis"; 1.29 + 1.30 + private static final String PREF_SCROLLING_FRICTION_SLOW = "ui.scrolling.friction_slow"; 1.31 + private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast"; 1.32 + private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration"; 1.33 + private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate"; 1.34 + private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit"; 1.35 + private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance"; 1.36 + 1.37 + // This fraction of velocity remains after every animation frame when the velocity is low. 1.38 + private static float FRICTION_SLOW; 1.39 + // This fraction of velocity remains after every animation frame when the velocity is high. 1.40 + private static float FRICTION_FAST; 1.41 + // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST 1.42 + // to FRICTION_SLOW. 1.43 + private static float VELOCITY_THRESHOLD; 1.44 + // The maximum velocity change factor between events, per ms, in %. 1.45 + // Direction changes are excluded. 1.46 + private static float MAX_EVENT_ACCELERATION; 1.47 + 1.48 + // The rate of deceleration when the surface has overscrolled. 1.49 + private static float OVERSCROLL_DECEL_RATE; 1.50 + // The percentage of the surface which can be overscrolled before it must snap back. 1.51 + private static float SNAP_LIMIT; 1.52 + 1.53 + // The minimum amount of space that must be present for an axis to be considered scrollable, 1.54 + // in pixels. 1.55 + private static float MIN_SCROLLABLE_DISTANCE; 1.56 + 1.57 + private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) { 1.58 + Integer value = (prefs == null ? null : prefs.get(prefName)); 1.59 + return (float)(value == null || value < 0 ? defaultValue : value) / 1000f; 1.60 + } 1.61 + 1.62 + private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) { 1.63 + Integer value = (prefs == null ? null : prefs.get(prefName)); 1.64 + return (value == null || value < 0 ? defaultValue : value); 1.65 + } 1.66 + 1.67 + static void initPrefs() { 1.68 + final String[] prefs = { PREF_SCROLLING_FRICTION_FAST, 1.69 + PREF_SCROLLING_FRICTION_SLOW, 1.70 + PREF_SCROLLING_MAX_EVENT_ACCELERATION, 1.71 + PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 1.72 + PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 1.73 + PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE }; 1.74 + 1.75 + PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() { 1.76 + Map<String, Integer> mPrefs = new HashMap<String, Integer>(); 1.77 + 1.78 + @Override public void prefValue(String name, int value) { 1.79 + mPrefs.put(name, value); 1.80 + } 1.81 + 1.82 + @Override public void finish() { 1.83 + setPrefs(mPrefs); 1.84 + } 1.85 + }); 1.86 + } 1.87 + 1.88 + static final float MS_PER_FRAME = 1000.0f / 60.0f; 1.89 + static final long NS_PER_FRAME = Math.round(1000000000f / 60f); 1.90 + private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME; 1.91 + private static final int FLING_VELOCITY_POINTS = 8; 1.92 + 1.93 + // The values we use for friction are based on a 16.6ms frame, adjust them to currentNsPerFrame: 1.94 + static float getFrameAdjustedFriction(float baseFriction, long currentNsPerFrame) { 1.95 + float framerateMultiplier = (float)currentNsPerFrame / NS_PER_FRAME; 1.96 + return (float)Math.pow(Math.E, (Math.log(baseFriction) / framerateMultiplier)); 1.97 + } 1.98 + 1.99 + static void setPrefs(Map<String, Integer> prefs) { 1.100 + FRICTION_SLOW = getFloatPref(prefs, PREF_SCROLLING_FRICTION_SLOW, 850); 1.101 + FRICTION_FAST = getFloatPref(prefs, PREF_SCROLLING_FRICTION_FAST, 970); 1.102 + VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER; 1.103 + MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, GeckoAppShell.getDpi() > 300 ? 100 : 40); 1.104 + OVERSCROLL_DECEL_RATE = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40); 1.105 + SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300); 1.106 + MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500); 1.107 + Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + "," 1.108 + + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE); 1.109 + } 1.110 + 1.111 + static { 1.112 + // set the scrolling parameters to default values on startup 1.113 + setPrefs(null); 1.114 + } 1.115 + 1.116 + private enum FlingStates { 1.117 + STOPPED, 1.118 + PANNING, 1.119 + FLINGING, 1.120 + } 1.121 + 1.122 + private enum Overscroll { 1.123 + NONE, 1.124 + MINUS, // Overscrolled in the negative direction 1.125 + PLUS, // Overscrolled in the positive direction 1.126 + BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen) 1.127 + } 1.128 + 1.129 + private final SubdocumentScrollHelper mSubscroller; 1.130 + 1.131 + private int mOverscrollMode; /* Default to only overscrolling if we're allowed to scroll in a direction */ 1.132 + private float mFirstTouchPos; /* Position of the first touch event on the current drag. */ 1.133 + private float mTouchPos; /* Position of the most recent touch event on the current drag. */ 1.134 + private float mLastTouchPos; /* Position of the touch event before touchPos. */ 1.135 + private float mVelocity; /* Velocity in this direction; pixels per animation frame. */ 1.136 + private float[] mRecentVelocities; /* Circular buffer of recent velocities since last touch start. */ 1.137 + private int mRecentVelocityCount; /* Number of values put into mRecentVelocities (unbounded). */ 1.138 + private boolean mScrollingDisabled; /* Whether movement on this axis is locked. */ 1.139 + private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */ 1.140 + private float mDisplacement; 1.141 + 1.142 + private FlingStates mFlingState = FlingStates.STOPPED; /* The fling state we're in on this axis. */ 1.143 + 1.144 + protected abstract float getOrigin(); 1.145 + protected abstract float getViewportLength(); 1.146 + protected abstract float getPageStart(); 1.147 + protected abstract float getPageLength(); 1.148 + protected abstract float getMarginStart(); 1.149 + protected abstract float getMarginEnd(); 1.150 + protected abstract boolean marginsHidden(); 1.151 + 1.152 + Axis(SubdocumentScrollHelper subscroller) { 1.153 + mSubscroller = subscroller; 1.154 + mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS; 1.155 + mRecentVelocities = new float[FLING_VELOCITY_POINTS]; 1.156 + } 1.157 + 1.158 + // Implementors can override these to show effects when the axis overscrolls 1.159 + protected void overscrollFling(float velocity) { } 1.160 + protected void overscrollPan(float displacement) { } 1.161 + 1.162 + public void setOverScrollMode(int overscrollMode) { 1.163 + mOverscrollMode = overscrollMode; 1.164 + } 1.165 + 1.166 + public int getOverScrollMode() { 1.167 + return mOverscrollMode; 1.168 + } 1.169 + 1.170 + private float getViewportEnd() { 1.171 + return getOrigin() + getViewportLength(); 1.172 + } 1.173 + 1.174 + private float getPageEnd() { 1.175 + return getPageStart() + getPageLength(); 1.176 + } 1.177 + 1.178 + void startTouch(float pos) { 1.179 + mVelocity = 0.0f; 1.180 + mScrollingDisabled = false; 1.181 + mFirstTouchPos = mTouchPos = mLastTouchPos = pos; 1.182 + mRecentVelocityCount = 0; 1.183 + } 1.184 + 1.185 + float panDistance(float currentPos) { 1.186 + return currentPos - mFirstTouchPos; 1.187 + } 1.188 + 1.189 + void setScrollingDisabled(boolean disabled) { 1.190 + mScrollingDisabled = disabled; 1.191 + } 1.192 + 1.193 + void saveTouchPos() { 1.194 + mLastTouchPos = mTouchPos; 1.195 + } 1.196 + 1.197 + void updateWithTouchAt(float pos, float timeDelta) { 1.198 + float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME; 1.199 + 1.200 + mRecentVelocities[mRecentVelocityCount % FLING_VELOCITY_POINTS] = newVelocity; 1.201 + mRecentVelocityCount++; 1.202 + 1.203 + // If there's a direction change, or current velocity is very low, 1.204 + // allow setting of the velocity outright. Otherwise, use the current 1.205 + // velocity and a maximum change factor to set the new velocity. 1.206 + boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER; 1.207 + boolean directionChange = (mVelocity > 0) != (newVelocity > 0); 1.208 + if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) { 1.209 + mVelocity = newVelocity; 1.210 + } else { 1.211 + float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION); 1.212 + mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity)); 1.213 + } 1.214 + 1.215 + mTouchPos = pos; 1.216 + } 1.217 + 1.218 + boolean overscrolled() { 1.219 + return getOverscroll() != Overscroll.NONE; 1.220 + } 1.221 + 1.222 + private Overscroll getOverscroll() { 1.223 + boolean minus = (getOrigin() < getPageStart()); 1.224 + boolean plus = (getViewportEnd() > getPageEnd()); 1.225 + if (minus && plus) { 1.226 + return Overscroll.BOTH; 1.227 + } else if (minus) { 1.228 + return Overscroll.MINUS; 1.229 + } else if (plus) { 1.230 + return Overscroll.PLUS; 1.231 + } else { 1.232 + return Overscroll.NONE; 1.233 + } 1.234 + } 1.235 + 1.236 + // Returns the amount that the page has been overscrolled. If the page hasn't been 1.237 + // overscrolled on this axis, returns 0. 1.238 + private float getExcess() { 1.239 + switch (getOverscroll()) { 1.240 + case MINUS: return getPageStart() - getOrigin(); 1.241 + case PLUS: return getViewportEnd() - getPageEnd(); 1.242 + case BOTH: return (getViewportEnd() - getPageEnd()) + (getPageStart() - getOrigin()); 1.243 + default: return 0.0f; 1.244 + } 1.245 + } 1.246 + 1.247 + /* 1.248 + * Returns true if the page is zoomed in to some degree along this axis such that scrolling is 1.249 + * possible and this axis has not been scroll locked while panning. Otherwise, returns false. 1.250 + */ 1.251 + boolean scrollable() { 1.252 + // If we're scrolling a subdocument, ignore the viewport length restrictions (since those 1.253 + // apply to the top-level document) and only take into account axis locking. 1.254 + if (mSubscroller.scrolling()) { 1.255 + return !mScrollingDisabled; 1.256 + } 1.257 + 1.258 + // if we are axis locked, return false 1.259 + if (mScrollingDisabled) { 1.260 + return false; 1.261 + } 1.262 + 1.263 + // if there are margins on this axis but they are currently hidden, 1.264 + // we must be able to scroll in order to make them visible, so allow 1.265 + // scrolling in that case 1.266 + if (marginsHidden()) { 1.267 + return true; 1.268 + } 1.269 + 1.270 + // there is scrollable space, and we're not disabled, or the document fits the viewport 1.271 + // but we always allow overscroll anyway 1.272 + return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE || 1.273 + getOverScrollMode() == View.OVER_SCROLL_ALWAYS; 1.274 + } 1.275 + 1.276 + /* 1.277 + * Returns the resistance, as a multiplier, that should be taken into account when 1.278 + * tracking or pinching. 1.279 + */ 1.280 + float getEdgeResistance(boolean forPinching) { 1.281 + float excess = getExcess(); 1.282 + if (excess > 0.0f && (getOverscroll() == Overscroll.BOTH || !forPinching)) { 1.283 + // excess can be greater than viewport length, but the resistance 1.284 + // must never drop below 0.0 1.285 + return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength()); 1.286 + } 1.287 + return 1.0f; 1.288 + } 1.289 + 1.290 + /* Returns the velocity. If the axis is locked, returns 0. */ 1.291 + float getRealVelocity() { 1.292 + return scrollable() ? mVelocity : 0f; 1.293 + } 1.294 + 1.295 + void startPan() { 1.296 + mFlingState = FlingStates.PANNING; 1.297 + } 1.298 + 1.299 + private float calculateFlingVelocity() { 1.300 + int usablePoints = Math.min(mRecentVelocityCount, FLING_VELOCITY_POINTS); 1.301 + if (usablePoints <= 1) { 1.302 + return mVelocity; 1.303 + } 1.304 + float average = 0; 1.305 + for (int i = 0; i < usablePoints; i++) { 1.306 + average += mRecentVelocities[i]; 1.307 + } 1.308 + return average / usablePoints; 1.309 + } 1.310 + 1.311 + void startFling(boolean stopped) { 1.312 + mDisableSnap = mSubscroller.scrolling(); 1.313 + 1.314 + if (stopped) { 1.315 + mFlingState = FlingStates.STOPPED; 1.316 + } else { 1.317 + mVelocity = calculateFlingVelocity(); 1.318 + mFlingState = FlingStates.FLINGING; 1.319 + } 1.320 + } 1.321 + 1.322 + /* Advances a fling animation by one step. */ 1.323 + boolean advanceFling(long realNsPerFrame) { 1.324 + if (mFlingState != FlingStates.FLINGING) { 1.325 + return false; 1.326 + } 1.327 + if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) { 1.328 + // if the subdocument stopped scrolling, it's because it reached the end 1.329 + // of the subdocument. we don't do overscroll on subdocuments, so there's 1.330 + // no point in continuing this fling. 1.331 + return false; 1.332 + } 1.333 + 1.334 + float excess = getExcess(); 1.335 + Overscroll overscroll = getOverscroll(); 1.336 + boolean decreasingOverscroll = false; 1.337 + if ((overscroll == Overscroll.MINUS && mVelocity > 0) || 1.338 + (overscroll == Overscroll.PLUS && mVelocity < 0)) 1.339 + { 1.340 + decreasingOverscroll = true; 1.341 + } 1.342 + 1.343 + if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f) || decreasingOverscroll) { 1.344 + // If we aren't overscrolled, just apply friction. 1.345 + if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) { 1.346 + mVelocity *= getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame); 1.347 + } else { 1.348 + float t = mVelocity / VELOCITY_THRESHOLD; 1.349 + mVelocity *= FloatUtils.interpolate(getFrameAdjustedFriction(FRICTION_SLOW, realNsPerFrame), 1.350 + getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame), t); 1.351 + } 1.352 + } else { 1.353 + // Otherwise, decrease the velocity linearly. 1.354 + float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT); 1.355 + float overscrollDecelRate = getFrameAdjustedFriction(OVERSCROLL_DECEL_RATE, realNsPerFrame); 1.356 + if (overscroll == Overscroll.MINUS) { 1.357 + mVelocity = Math.min((mVelocity + overscrollDecelRate) * elasticity, 0.0f); 1.358 + } else { // must be Overscroll.PLUS 1.359 + mVelocity = Math.max((mVelocity - overscrollDecelRate) * elasticity, 0.0f); 1.360 + } 1.361 + } 1.362 + 1.363 + return true; 1.364 + } 1.365 + 1.366 + void stopFling() { 1.367 + mVelocity = 0.0f; 1.368 + mFlingState = FlingStates.STOPPED; 1.369 + } 1.370 + 1.371 + // Performs displacement of the viewport position according to the current velocity. 1.372 + void displace() { 1.373 + // if this isn't scrollable just return 1.374 + if (!scrollable()) 1.375 + return; 1.376 + 1.377 + if (mFlingState == FlingStates.PANNING) 1.378 + mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(false); 1.379 + else 1.380 + mDisplacement += mVelocity * getEdgeResistance(false); 1.381 + 1.382 + // if overscroll is disabled and we're trying to overscroll, reset the displacement 1.383 + // to remove any excess. Using getExcess alone isn't enough here since it relies on 1.384 + // getOverscroll which doesn't take into account any new displacment being applied. 1.385 + // If we using a subscroller, we don't want to alter the scrolling being done 1.386 + if (getOverScrollMode() == View.OVER_SCROLL_NEVER && !mSubscroller.scrolling()) { 1.387 + float originalDisplacement = mDisplacement; 1.388 + 1.389 + if (mDisplacement + getOrigin() < getPageStart() - getMarginStart()) { 1.390 + mDisplacement = getPageStart() - getMarginStart() - getOrigin(); 1.391 + } else if (mDisplacement + getViewportEnd() > getPageEnd() + getMarginEnd()) { 1.392 + mDisplacement = getPageEnd() - getMarginEnd() - getViewportEnd(); 1.393 + } 1.394 + 1.395 + // Return the amount of overscroll so that the overscroll controller can draw it for us 1.396 + if (originalDisplacement != mDisplacement) { 1.397 + if (mFlingState == FlingStates.FLINGING) { 1.398 + overscrollFling(mVelocity / MS_PER_FRAME * 1000); 1.399 + stopFling(); 1.400 + } else if (mFlingState == FlingStates.PANNING) { 1.401 + overscrollPan(originalDisplacement - mDisplacement); 1.402 + } 1.403 + } 1.404 + } 1.405 + } 1.406 + 1.407 + float resetDisplacement() { 1.408 + float d = mDisplacement; 1.409 + mDisplacement = 0.0f; 1.410 + return d; 1.411 + } 1.412 + 1.413 + void setAutoscrollVelocity(float velocity) { 1.414 + if (mFlingState != FlingStates.STOPPED) { 1.415 + Log.e(LOGTAG, "Setting autoscroll velocity while in a fling is not allowed!"); 1.416 + return; 1.417 + } 1.418 + mVelocity = velocity; 1.419 + } 1.420 +}