mobile/android/base/gfx/Axis.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko.gfx;
michael@0 7
michael@0 8 import java.util.HashMap;
michael@0 9 import java.util.Map;
michael@0 10
michael@0 11 import org.mozilla.gecko.GeckoAppShell;
michael@0 12 import org.mozilla.gecko.PrefsHelper;
michael@0 13 import org.mozilla.gecko.util.FloatUtils;
michael@0 14
michael@0 15 import android.util.Log;
michael@0 16 import android.view.View;
michael@0 17
michael@0 18 /**
michael@0 19 * This class represents the physics for one axis of movement (i.e. either
michael@0 20 * horizontal or vertical). It tracks the different properties of movement
michael@0 21 * like displacement, velocity, viewport dimensions, etc. pertaining to
michael@0 22 * a particular axis.
michael@0 23 */
michael@0 24 abstract class Axis {
michael@0 25 private static final String LOGTAG = "GeckoAxis";
michael@0 26
michael@0 27 private static final String PREF_SCROLLING_FRICTION_SLOW = "ui.scrolling.friction_slow";
michael@0 28 private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast";
michael@0 29 private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration";
michael@0 30 private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate";
michael@0 31 private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit";
michael@0 32 private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance";
michael@0 33
michael@0 34 // This fraction of velocity remains after every animation frame when the velocity is low.
michael@0 35 private static float FRICTION_SLOW;
michael@0 36 // This fraction of velocity remains after every animation frame when the velocity is high.
michael@0 37 private static float FRICTION_FAST;
michael@0 38 // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST
michael@0 39 // to FRICTION_SLOW.
michael@0 40 private static float VELOCITY_THRESHOLD;
michael@0 41 // The maximum velocity change factor between events, per ms, in %.
michael@0 42 // Direction changes are excluded.
michael@0 43 private static float MAX_EVENT_ACCELERATION;
michael@0 44
michael@0 45 // The rate of deceleration when the surface has overscrolled.
michael@0 46 private static float OVERSCROLL_DECEL_RATE;
michael@0 47 // The percentage of the surface which can be overscrolled before it must snap back.
michael@0 48 private static float SNAP_LIMIT;
michael@0 49
michael@0 50 // The minimum amount of space that must be present for an axis to be considered scrollable,
michael@0 51 // in pixels.
michael@0 52 private static float MIN_SCROLLABLE_DISTANCE;
michael@0 53
michael@0 54 private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
michael@0 55 Integer value = (prefs == null ? null : prefs.get(prefName));
michael@0 56 return (float)(value == null || value < 0 ? defaultValue : value) / 1000f;
michael@0 57 }
michael@0 58
michael@0 59 private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) {
michael@0 60 Integer value = (prefs == null ? null : prefs.get(prefName));
michael@0 61 return (value == null || value < 0 ? defaultValue : value);
michael@0 62 }
michael@0 63
michael@0 64 static void initPrefs() {
michael@0 65 final String[] prefs = { PREF_SCROLLING_FRICTION_FAST,
michael@0 66 PREF_SCROLLING_FRICTION_SLOW,
michael@0 67 PREF_SCROLLING_MAX_EVENT_ACCELERATION,
michael@0 68 PREF_SCROLLING_OVERSCROLL_DECEL_RATE,
michael@0 69 PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT,
michael@0 70 PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE };
michael@0 71
michael@0 72 PrefsHelper.getPrefs(prefs, new PrefsHelper.PrefHandlerBase() {
michael@0 73 Map<String, Integer> mPrefs = new HashMap<String, Integer>();
michael@0 74
michael@0 75 @Override public void prefValue(String name, int value) {
michael@0 76 mPrefs.put(name, value);
michael@0 77 }
michael@0 78
michael@0 79 @Override public void finish() {
michael@0 80 setPrefs(mPrefs);
michael@0 81 }
michael@0 82 });
michael@0 83 }
michael@0 84
michael@0 85 static final float MS_PER_FRAME = 1000.0f / 60.0f;
michael@0 86 static final long NS_PER_FRAME = Math.round(1000000000f / 60f);
michael@0 87 private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME;
michael@0 88 private static final int FLING_VELOCITY_POINTS = 8;
michael@0 89
michael@0 90 // The values we use for friction are based on a 16.6ms frame, adjust them to currentNsPerFrame:
michael@0 91 static float getFrameAdjustedFriction(float baseFriction, long currentNsPerFrame) {
michael@0 92 float framerateMultiplier = (float)currentNsPerFrame / NS_PER_FRAME;
michael@0 93 return (float)Math.pow(Math.E, (Math.log(baseFriction) / framerateMultiplier));
michael@0 94 }
michael@0 95
michael@0 96 static void setPrefs(Map<String, Integer> prefs) {
michael@0 97 FRICTION_SLOW = getFloatPref(prefs, PREF_SCROLLING_FRICTION_SLOW, 850);
michael@0 98 FRICTION_FAST = getFloatPref(prefs, PREF_SCROLLING_FRICTION_FAST, 970);
michael@0 99 VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER;
michael@0 100 MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, GeckoAppShell.getDpi() > 300 ? 100 : 40);
michael@0 101 OVERSCROLL_DECEL_RATE = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40);
michael@0 102 SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300);
michael@0 103 MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500);
michael@0 104 Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + ","
michael@0 105 + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE);
michael@0 106 }
michael@0 107
michael@0 108 static {
michael@0 109 // set the scrolling parameters to default values on startup
michael@0 110 setPrefs(null);
michael@0 111 }
michael@0 112
michael@0 113 private enum FlingStates {
michael@0 114 STOPPED,
michael@0 115 PANNING,
michael@0 116 FLINGING,
michael@0 117 }
michael@0 118
michael@0 119 private enum Overscroll {
michael@0 120 NONE,
michael@0 121 MINUS, // Overscrolled in the negative direction
michael@0 122 PLUS, // Overscrolled in the positive direction
michael@0 123 BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen)
michael@0 124 }
michael@0 125
michael@0 126 private final SubdocumentScrollHelper mSubscroller;
michael@0 127
michael@0 128 private int mOverscrollMode; /* Default to only overscrolling if we're allowed to scroll in a direction */
michael@0 129 private float mFirstTouchPos; /* Position of the first touch event on the current drag. */
michael@0 130 private float mTouchPos; /* Position of the most recent touch event on the current drag. */
michael@0 131 private float mLastTouchPos; /* Position of the touch event before touchPos. */
michael@0 132 private float mVelocity; /* Velocity in this direction; pixels per animation frame. */
michael@0 133 private float[] mRecentVelocities; /* Circular buffer of recent velocities since last touch start. */
michael@0 134 private int mRecentVelocityCount; /* Number of values put into mRecentVelocities (unbounded). */
michael@0 135 private boolean mScrollingDisabled; /* Whether movement on this axis is locked. */
michael@0 136 private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */
michael@0 137 private float mDisplacement;
michael@0 138
michael@0 139 private FlingStates mFlingState = FlingStates.STOPPED; /* The fling state we're in on this axis. */
michael@0 140
michael@0 141 protected abstract float getOrigin();
michael@0 142 protected abstract float getViewportLength();
michael@0 143 protected abstract float getPageStart();
michael@0 144 protected abstract float getPageLength();
michael@0 145 protected abstract float getMarginStart();
michael@0 146 protected abstract float getMarginEnd();
michael@0 147 protected abstract boolean marginsHidden();
michael@0 148
michael@0 149 Axis(SubdocumentScrollHelper subscroller) {
michael@0 150 mSubscroller = subscroller;
michael@0 151 mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
michael@0 152 mRecentVelocities = new float[FLING_VELOCITY_POINTS];
michael@0 153 }
michael@0 154
michael@0 155 // Implementors can override these to show effects when the axis overscrolls
michael@0 156 protected void overscrollFling(float velocity) { }
michael@0 157 protected void overscrollPan(float displacement) { }
michael@0 158
michael@0 159 public void setOverScrollMode(int overscrollMode) {
michael@0 160 mOverscrollMode = overscrollMode;
michael@0 161 }
michael@0 162
michael@0 163 public int getOverScrollMode() {
michael@0 164 return mOverscrollMode;
michael@0 165 }
michael@0 166
michael@0 167 private float getViewportEnd() {
michael@0 168 return getOrigin() + getViewportLength();
michael@0 169 }
michael@0 170
michael@0 171 private float getPageEnd() {
michael@0 172 return getPageStart() + getPageLength();
michael@0 173 }
michael@0 174
michael@0 175 void startTouch(float pos) {
michael@0 176 mVelocity = 0.0f;
michael@0 177 mScrollingDisabled = false;
michael@0 178 mFirstTouchPos = mTouchPos = mLastTouchPos = pos;
michael@0 179 mRecentVelocityCount = 0;
michael@0 180 }
michael@0 181
michael@0 182 float panDistance(float currentPos) {
michael@0 183 return currentPos - mFirstTouchPos;
michael@0 184 }
michael@0 185
michael@0 186 void setScrollingDisabled(boolean disabled) {
michael@0 187 mScrollingDisabled = disabled;
michael@0 188 }
michael@0 189
michael@0 190 void saveTouchPos() {
michael@0 191 mLastTouchPos = mTouchPos;
michael@0 192 }
michael@0 193
michael@0 194 void updateWithTouchAt(float pos, float timeDelta) {
michael@0 195 float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME;
michael@0 196
michael@0 197 mRecentVelocities[mRecentVelocityCount % FLING_VELOCITY_POINTS] = newVelocity;
michael@0 198 mRecentVelocityCount++;
michael@0 199
michael@0 200 // If there's a direction change, or current velocity is very low,
michael@0 201 // allow setting of the velocity outright. Otherwise, use the current
michael@0 202 // velocity and a maximum change factor to set the new velocity.
michael@0 203 boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER;
michael@0 204 boolean directionChange = (mVelocity > 0) != (newVelocity > 0);
michael@0 205 if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) {
michael@0 206 mVelocity = newVelocity;
michael@0 207 } else {
michael@0 208 float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION);
michael@0 209 mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity));
michael@0 210 }
michael@0 211
michael@0 212 mTouchPos = pos;
michael@0 213 }
michael@0 214
michael@0 215 boolean overscrolled() {
michael@0 216 return getOverscroll() != Overscroll.NONE;
michael@0 217 }
michael@0 218
michael@0 219 private Overscroll getOverscroll() {
michael@0 220 boolean minus = (getOrigin() < getPageStart());
michael@0 221 boolean plus = (getViewportEnd() > getPageEnd());
michael@0 222 if (minus && plus) {
michael@0 223 return Overscroll.BOTH;
michael@0 224 } else if (minus) {
michael@0 225 return Overscroll.MINUS;
michael@0 226 } else if (plus) {
michael@0 227 return Overscroll.PLUS;
michael@0 228 } else {
michael@0 229 return Overscroll.NONE;
michael@0 230 }
michael@0 231 }
michael@0 232
michael@0 233 // Returns the amount that the page has been overscrolled. If the page hasn't been
michael@0 234 // overscrolled on this axis, returns 0.
michael@0 235 private float getExcess() {
michael@0 236 switch (getOverscroll()) {
michael@0 237 case MINUS: return getPageStart() - getOrigin();
michael@0 238 case PLUS: return getViewportEnd() - getPageEnd();
michael@0 239 case BOTH: return (getViewportEnd() - getPageEnd()) + (getPageStart() - getOrigin());
michael@0 240 default: return 0.0f;
michael@0 241 }
michael@0 242 }
michael@0 243
michael@0 244 /*
michael@0 245 * Returns true if the page is zoomed in to some degree along this axis such that scrolling is
michael@0 246 * possible and this axis has not been scroll locked while panning. Otherwise, returns false.
michael@0 247 */
michael@0 248 boolean scrollable() {
michael@0 249 // If we're scrolling a subdocument, ignore the viewport length restrictions (since those
michael@0 250 // apply to the top-level document) and only take into account axis locking.
michael@0 251 if (mSubscroller.scrolling()) {
michael@0 252 return !mScrollingDisabled;
michael@0 253 }
michael@0 254
michael@0 255 // if we are axis locked, return false
michael@0 256 if (mScrollingDisabled) {
michael@0 257 return false;
michael@0 258 }
michael@0 259
michael@0 260 // if there are margins on this axis but they are currently hidden,
michael@0 261 // we must be able to scroll in order to make them visible, so allow
michael@0 262 // scrolling in that case
michael@0 263 if (marginsHidden()) {
michael@0 264 return true;
michael@0 265 }
michael@0 266
michael@0 267 // there is scrollable space, and we're not disabled, or the document fits the viewport
michael@0 268 // but we always allow overscroll anyway
michael@0 269 return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE ||
michael@0 270 getOverScrollMode() == View.OVER_SCROLL_ALWAYS;
michael@0 271 }
michael@0 272
michael@0 273 /*
michael@0 274 * Returns the resistance, as a multiplier, that should be taken into account when
michael@0 275 * tracking or pinching.
michael@0 276 */
michael@0 277 float getEdgeResistance(boolean forPinching) {
michael@0 278 float excess = getExcess();
michael@0 279 if (excess > 0.0f && (getOverscroll() == Overscroll.BOTH || !forPinching)) {
michael@0 280 // excess can be greater than viewport length, but the resistance
michael@0 281 // must never drop below 0.0
michael@0 282 return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength());
michael@0 283 }
michael@0 284 return 1.0f;
michael@0 285 }
michael@0 286
michael@0 287 /* Returns the velocity. If the axis is locked, returns 0. */
michael@0 288 float getRealVelocity() {
michael@0 289 return scrollable() ? mVelocity : 0f;
michael@0 290 }
michael@0 291
michael@0 292 void startPan() {
michael@0 293 mFlingState = FlingStates.PANNING;
michael@0 294 }
michael@0 295
michael@0 296 private float calculateFlingVelocity() {
michael@0 297 int usablePoints = Math.min(mRecentVelocityCount, FLING_VELOCITY_POINTS);
michael@0 298 if (usablePoints <= 1) {
michael@0 299 return mVelocity;
michael@0 300 }
michael@0 301 float average = 0;
michael@0 302 for (int i = 0; i < usablePoints; i++) {
michael@0 303 average += mRecentVelocities[i];
michael@0 304 }
michael@0 305 return average / usablePoints;
michael@0 306 }
michael@0 307
michael@0 308 void startFling(boolean stopped) {
michael@0 309 mDisableSnap = mSubscroller.scrolling();
michael@0 310
michael@0 311 if (stopped) {
michael@0 312 mFlingState = FlingStates.STOPPED;
michael@0 313 } else {
michael@0 314 mVelocity = calculateFlingVelocity();
michael@0 315 mFlingState = FlingStates.FLINGING;
michael@0 316 }
michael@0 317 }
michael@0 318
michael@0 319 /* Advances a fling animation by one step. */
michael@0 320 boolean advanceFling(long realNsPerFrame) {
michael@0 321 if (mFlingState != FlingStates.FLINGING) {
michael@0 322 return false;
michael@0 323 }
michael@0 324 if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) {
michael@0 325 // if the subdocument stopped scrolling, it's because it reached the end
michael@0 326 // of the subdocument. we don't do overscroll on subdocuments, so there's
michael@0 327 // no point in continuing this fling.
michael@0 328 return false;
michael@0 329 }
michael@0 330
michael@0 331 float excess = getExcess();
michael@0 332 Overscroll overscroll = getOverscroll();
michael@0 333 boolean decreasingOverscroll = false;
michael@0 334 if ((overscroll == Overscroll.MINUS && mVelocity > 0) ||
michael@0 335 (overscroll == Overscroll.PLUS && mVelocity < 0))
michael@0 336 {
michael@0 337 decreasingOverscroll = true;
michael@0 338 }
michael@0 339
michael@0 340 if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f) || decreasingOverscroll) {
michael@0 341 // If we aren't overscrolled, just apply friction.
michael@0 342 if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) {
michael@0 343 mVelocity *= getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame);
michael@0 344 } else {
michael@0 345 float t = mVelocity / VELOCITY_THRESHOLD;
michael@0 346 mVelocity *= FloatUtils.interpolate(getFrameAdjustedFriction(FRICTION_SLOW, realNsPerFrame),
michael@0 347 getFrameAdjustedFriction(FRICTION_FAST, realNsPerFrame), t);
michael@0 348 }
michael@0 349 } else {
michael@0 350 // Otherwise, decrease the velocity linearly.
michael@0 351 float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT);
michael@0 352 float overscrollDecelRate = getFrameAdjustedFriction(OVERSCROLL_DECEL_RATE, realNsPerFrame);
michael@0 353 if (overscroll == Overscroll.MINUS) {
michael@0 354 mVelocity = Math.min((mVelocity + overscrollDecelRate) * elasticity, 0.0f);
michael@0 355 } else { // must be Overscroll.PLUS
michael@0 356 mVelocity = Math.max((mVelocity - overscrollDecelRate) * elasticity, 0.0f);
michael@0 357 }
michael@0 358 }
michael@0 359
michael@0 360 return true;
michael@0 361 }
michael@0 362
michael@0 363 void stopFling() {
michael@0 364 mVelocity = 0.0f;
michael@0 365 mFlingState = FlingStates.STOPPED;
michael@0 366 }
michael@0 367
michael@0 368 // Performs displacement of the viewport position according to the current velocity.
michael@0 369 void displace() {
michael@0 370 // if this isn't scrollable just return
michael@0 371 if (!scrollable())
michael@0 372 return;
michael@0 373
michael@0 374 if (mFlingState == FlingStates.PANNING)
michael@0 375 mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(false);
michael@0 376 else
michael@0 377 mDisplacement += mVelocity * getEdgeResistance(false);
michael@0 378
michael@0 379 // if overscroll is disabled and we're trying to overscroll, reset the displacement
michael@0 380 // to remove any excess. Using getExcess alone isn't enough here since it relies on
michael@0 381 // getOverscroll which doesn't take into account any new displacment being applied.
michael@0 382 // If we using a subscroller, we don't want to alter the scrolling being done
michael@0 383 if (getOverScrollMode() == View.OVER_SCROLL_NEVER && !mSubscroller.scrolling()) {
michael@0 384 float originalDisplacement = mDisplacement;
michael@0 385
michael@0 386 if (mDisplacement + getOrigin() < getPageStart() - getMarginStart()) {
michael@0 387 mDisplacement = getPageStart() - getMarginStart() - getOrigin();
michael@0 388 } else if (mDisplacement + getViewportEnd() > getPageEnd() + getMarginEnd()) {
michael@0 389 mDisplacement = getPageEnd() - getMarginEnd() - getViewportEnd();
michael@0 390 }
michael@0 391
michael@0 392 // Return the amount of overscroll so that the overscroll controller can draw it for us
michael@0 393 if (originalDisplacement != mDisplacement) {
michael@0 394 if (mFlingState == FlingStates.FLINGING) {
michael@0 395 overscrollFling(mVelocity / MS_PER_FRAME * 1000);
michael@0 396 stopFling();
michael@0 397 } else if (mFlingState == FlingStates.PANNING) {
michael@0 398 overscrollPan(originalDisplacement - mDisplacement);
michael@0 399 }
michael@0 400 }
michael@0 401 }
michael@0 402 }
michael@0 403
michael@0 404 float resetDisplacement() {
michael@0 405 float d = mDisplacement;
michael@0 406 mDisplacement = 0.0f;
michael@0 407 return d;
michael@0 408 }
michael@0 409
michael@0 410 void setAutoscrollVelocity(float velocity) {
michael@0 411 if (mFlingState != FlingStates.STOPPED) {
michael@0 412 Log.e(LOGTAG, "Setting autoscroll velocity while in a fling is not allowed!");
michael@0 413 return;
michael@0 414 }
michael@0 415 mVelocity = velocity;
michael@0 416 }
michael@0 417 }

mercurial