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/. */
6 package org
.mozilla
.gecko
.gfx
;
8 import android
.util
.Log
;
9 import android
.view
.View
;
11 import org
.mozilla
.gecko
.util
.FloatUtils
;
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
22 private static final String LOGTAG
= "GeckoAxis";
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";
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
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
;
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
;
47 // The minimum amount of space that must be present for an axis to be considered scrollable,
49 private static float MIN_SCROLLABLE_DISTANCE
;
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
;
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
);
61 static final float MS_PER_FRAME
= 4.0f
;
62 private static final float FRAMERATE_MULTIPLIER
= (1000f
/60f
) / MS_PER_FRAME
;
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)
67 static float getFrameAdjustedFriction(float baseFriction
) {
68 return (float)Math
.pow(Math
.E
, (Math
.log(baseFriction
) / FRAMERATE_MULTIPLIER
));
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
);
84 // set the scrolling parameters to default values on startup
88 private enum FlingStates
{
94 private enum Overscroll
{
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)
101 private final SubdocumentScrollHelper mSubscroller
;
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
;
112 private FlingStates mFlingState
; /* The fling state we're in on this axis. */
114 protected abstract float getOrigin();
115 protected abstract float getViewportLength();
116 protected abstract float getPageStart();
117 protected abstract float getPageLength();
119 Axis(SubdocumentScrollHelper subscroller
) {
120 mSubscroller
= subscroller
;
121 mOverscrollMode
= View
.OVER_SCROLL_IF_CONTENT_SCROLLS
;
124 public void setOverScrollMode(int overscrollMode
) {
125 mOverscrollMode
= overscrollMode
;
128 public int getOverScrollMode() {
129 return mOverscrollMode
;
132 private float getViewportEnd() {
133 return getOrigin() + getViewportLength();
136 private float getPageEnd() {
137 return getPageStart() + getPageLength();
140 void startTouch(float pos
) {
142 mScrollingDisabled
= false;
143 mFirstTouchPos
= mTouchPos
= mLastTouchPos
= pos
;
146 float panDistance(float currentPos
) {
147 return currentPos
- mFirstTouchPos
;
150 void setScrollingDisabled(boolean disabled
) {
151 mScrollingDisabled
= disabled
;
154 void saveTouchPos() {
155 mLastTouchPos
= mTouchPos
;
158 void updateWithTouchAt(float pos
, float timeDelta
) {
159 float newVelocity
= (mTouchPos
- pos
) / timeDelta
* MS_PER_FRAME
;
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
;
169 float maxChange
= Math
.abs(mVelocity
* timeDelta
* MAX_EVENT_ACCELERATION
);
170 mVelocity
= Math
.min(mVelocity
+ maxChange
, Math
.max(mVelocity
- maxChange
, newVelocity
));
176 boolean overscrolled() {
177 return getOverscroll() != Overscroll
.NONE
;
180 private Overscroll
getOverscroll() {
181 boolean minus
= (getOrigin() < getPageStart());
182 boolean plus
= (getViewportEnd() > getPageEnd());
184 return Overscroll
.BOTH
;
186 return Overscroll
.MINUS
;
188 return Overscroll
.PLUS
;
190 return Overscroll
.NONE
;
194 // Returns the amount that the page has been overscrolled. If the page hasn't been
195 // overscrolled on this axis, returns 0.
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
;
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.
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
;
216 // if we are axis locked, return false
217 if (mScrollingDisabled
) {
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
;
228 * Returns the resistance, as a multiplier, that should be taken into account when
229 * tracking or pinching.
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());
241 /* Returns the velocity. If the axis is locked, returns 0. */
242 float getRealVelocity() {
243 return scrollable() ? mVelocity
: 0f
;
247 mFlingState
= FlingStates
.PANNING
;
250 void startFling(boolean stopped
) {
251 mDisableSnap
= mSubscroller
.scrolling();
254 mFlingState
= FlingStates
.STOPPED
;
256 mFlingState
= FlingStates
.FLINGING
;
260 /* Advances a fling animation by one step. */
261 boolean advanceFling() {
262 if (mFlingState
!= FlingStates
.FLINGING
) {
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.
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))
278 decreasingOverscroll
= true;
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
;
286 float t
= mVelocity
/ VELOCITY_THRESHOLD
;
287 mVelocity
*= FloatUtils
.interpolate(FRICTION_SLOW
, FRICTION_FAST
, t
);
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
);
304 mFlingState
= FlingStates
.STOPPED
;
307 // Performs displacement of the viewport position according to the current velocity.
309 // if this isn't scrollable just return
313 if (mFlingState
== FlingStates
.PANNING
)
314 mDisplacement
+= (mLastTouchPos
- mTouchPos
) * getEdgeResistance(false);
316 mDisplacement
+= mVelocity
;
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();
325 } else if (mDisplacement
+ getViewportEnd() > getPageEnd()) {
326 mDisplacement
= getPageEnd() - getViewportEnd();
332 float resetDisplacement() {
333 float d
= mDisplacement
;
334 mDisplacement
= 0.0f
;