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