1package com.android.launcher3.allapps; 2 3import android.content.Context; 4import android.util.Log; 5import android.view.MotionEvent; 6import android.view.ViewConfiguration; 7import android.view.animation.Interpolator; 8 9/** 10 * One dimensional scroll gesture detector for all apps container pull up interaction. 11 * Client (e.g., AllAppsTransitionController) of this class can register a listener. 12 * <p/> 13 * Features that this gesture detector can support. 14 */ 15public class VerticalPullDetector { 16 17 private static final boolean DBG = false; 18 private static final String TAG = "VerticalPullDetector"; 19 20 private float mTouchSlop; 21 22 private int mScrollConditions; 23 public static final int DIRECTION_UP = 1 << 0; 24 public static final int DIRECTION_DOWN = 1 << 1; 25 public static final int DIRECTION_BOTH = DIRECTION_DOWN | DIRECTION_UP; 26 27 private static final float ANIMATION_DURATION = 1200; 28 private static final float FAST_FLING_PX_MS = 10; 29 30 /** 31 * The minimum release velocity in pixels per millisecond that triggers fling.. 32 */ 33 public static final float RELEASE_VELOCITY_PX_MS = 1.0f; 34 35 /** 36 * The time constant used to calculate dampening in the low-pass filter of scroll velocity. 37 * Cutoff frequency is set at 10 Hz. 38 */ 39 public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10); 40 41 /* Scroll state, this is set to true during dragging and animation. */ 42 private ScrollState mState = ScrollState.IDLE; 43 44 enum ScrollState { 45 IDLE, 46 DRAGGING, // onDragStart, onDrag 47 SETTLING // onDragEnd 48 } 49 50 ; 51 52 //------------------- ScrollState transition diagram ----------------------------------- 53 // 54 // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING 55 // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING 56 // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING 57 // SETTLING -> (View settled) -> IDLE 58 59 private void setState(ScrollState newState) { 60 if (DBG) { 61 Log.d(TAG, "setState:" + mState + "->" + newState); 62 } 63 // onDragStart and onDragEnd is reported ONLY on state transition 64 if (newState == ScrollState.DRAGGING) { 65 initializeDragging(); 66 if (mState == ScrollState.IDLE) { 67 reportDragStart(false /* recatch */); 68 } else if (mState == ScrollState.SETTLING) { 69 reportDragStart(true /* recatch */); 70 } 71 } 72 if (newState == ScrollState.SETTLING) { 73 reportDragEnd(); 74 } 75 76 mState = newState; 77 } 78 79 public boolean isDraggingOrSettling() { 80 return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; 81 } 82 83 /** 84 * There's no touch and there's no animation. 85 */ 86 public boolean isIdleState() { 87 return mState == ScrollState.IDLE; 88 } 89 90 public boolean isSettlingState() { 91 return mState == ScrollState.SETTLING; 92 } 93 94 public boolean isDraggingState() { 95 return mState == ScrollState.DRAGGING; 96 } 97 98 private float mDownX; 99 private float mDownY; 100 101 private float mLastY; 102 private long mCurrentMillis; 103 104 private float mVelocity; 105 private float mLastDisplacement; 106 private float mDisplacementY; 107 private float mDisplacementX; 108 109 private float mSubtractDisplacement; 110 private boolean mIgnoreSlopWhenSettling; 111 112 /* Client of this gesture detector can register a callback. */ 113 Listener mListener; 114 115 public void setListener(Listener l) { 116 mListener = l; 117 } 118 119 public interface Listener { 120 void onDragStart(boolean start); 121 122 boolean onDrag(float displacement, float velocity); 123 124 void onDragEnd(float velocity, boolean fling); 125 } 126 127 public VerticalPullDetector(Context context) { 128 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 129 } 130 131 public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) { 132 mScrollConditions = scrollDirectionFlags; 133 mIgnoreSlopWhenSettling = ignoreSlop; 134 } 135 136 private boolean shouldScrollStart() { 137 // reject cases where the slop condition is not met. 138 if (Math.abs(mDisplacementY) < mTouchSlop) { 139 return false; 140 } 141 142 // reject cases where the angle condition is not met. 143 float deltaY = Math.abs(mDisplacementY); 144 float deltaX = Math.max(Math.abs(mDisplacementX), 1); 145 if (deltaX > deltaY) { 146 return false; 147 } 148 // Check if the client is interested in scroll in current direction. 149 if (((mScrollConditions & DIRECTION_DOWN) > 0 && mDisplacementY > 0) || 150 ((mScrollConditions & DIRECTION_UP) > 0 && mDisplacementY < 0)) { 151 return true; 152 } 153 return false; 154 } 155 156 public boolean onTouchEvent(MotionEvent ev) { 157 switch (ev.getAction()) { 158 case MotionEvent.ACTION_DOWN: 159 mDownX = ev.getX(); 160 mDownY = ev.getY(); 161 mLastDisplacement = 0; 162 mDisplacementY = 0; 163 mVelocity = 0; 164 165 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 166 setState(ScrollState.DRAGGING); 167 } 168 break; 169 case MotionEvent.ACTION_MOVE: 170 mDisplacementX = ev.getX() - mDownX; 171 mDisplacementY = ev.getY() - mDownY; 172 computeVelocity(ev); 173 174 // handle state and listener calls. 175 if (mState != ScrollState.DRAGGING && shouldScrollStart()) { 176 setState(ScrollState.DRAGGING); 177 } 178 if (mState == ScrollState.DRAGGING) { 179 reportDragging(); 180 } 181 break; 182 case MotionEvent.ACTION_CANCEL: 183 case MotionEvent.ACTION_UP: 184 // These are synthetic events and there is no need to update internal values. 185 if (mState == ScrollState.DRAGGING) { 186 setState(ScrollState.SETTLING); 187 } 188 break; 189 default: 190 //TODO: add multi finger tracking by tracking active pointer. 191 break; 192 } 193 // Do house keeping. 194 mLastDisplacement = mDisplacementY; 195 mLastY = ev.getY(); 196 return true; 197 } 198 199 public void finishedScrolling() { 200 setState(ScrollState.IDLE); 201 } 202 203 private boolean reportDragStart(boolean recatch) { 204 mListener.onDragStart(!recatch); 205 if (DBG) { 206 Log.d(TAG, "onDragStart recatch:" + recatch); 207 } 208 return true; 209 } 210 211 private void initializeDragging() { 212 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 213 mSubtractDisplacement = 0; 214 } 215 if (mDisplacementY > 0) { 216 mSubtractDisplacement = mTouchSlop; 217 } else { 218 mSubtractDisplacement = -mTouchSlop; 219 } 220 } 221 222 private boolean reportDragging() { 223 float delta = mDisplacementY - mLastDisplacement; 224 if (delta != 0) { 225 if (DBG) { 226 Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f", 227 mDisplacementY, mVelocity)); 228 } 229 230 return mListener.onDrag(mDisplacementY - mSubtractDisplacement, mVelocity); 231 } 232 return true; 233 } 234 235 private void reportDragEnd() { 236 if (DBG) { 237 Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f", 238 mDisplacementY, mVelocity)); 239 } 240 mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS); 241 242 } 243 244 /** 245 * Computes the damped velocity using the two motion events and the previous velocity. 246 */ 247 private float computeVelocity(MotionEvent to) { 248 return computeVelocity(to.getY() - mLastY, to.getEventTime()); 249 } 250 251 public float computeVelocity(float delta, long currentMillis) { 252 long previousMillis = mCurrentMillis; 253 mCurrentMillis = currentMillis; 254 255 float deltaTimeMillis = mCurrentMillis - previousMillis; 256 float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0; 257 if (Math.abs(mVelocity) < 0.001f) { 258 mVelocity = velocity; 259 } else { 260 float alpha = computeDampeningFactor(deltaTimeMillis); 261 mVelocity = interpolate(mVelocity, velocity, alpha); 262 } 263 return mVelocity; 264 } 265 266 /** 267 * Returns a time-dependent dampening factor using delta time. 268 */ 269 private static float computeDampeningFactor(float deltaTime) { 270 return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime); 271 } 272 273 /** 274 * Returns the linear interpolation between two values 275 */ 276 private static float interpolate(float from, float to, float alpha) { 277 return (1.0f - alpha) * from + alpha * to; 278 } 279 280 public long calculateDuration(float velocity, float progressNeeded) { 281 // TODO: make these values constants after tuning. 282 float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity)); 283 float travelDistance = Math.max(0.2f, progressNeeded); 284 long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); 285 if (DBG) { 286 Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded)); 287 } 288 return duration; 289 } 290 291 public static class ScrollInterpolator implements Interpolator { 292 293 boolean mSteeper; 294 295 public void setVelocityAtZero(float velocity) { 296 mSteeper = velocity > FAST_FLING_PX_MS; 297 } 298 299 public float getInterpolation(float t) { 300 t -= 1.0f; 301 float output = t * t * t; 302 if (mSteeper) { 303 output *= t * t; // Make interpolation initial slope steeper 304 } 305 return output + 1; 306 } 307 } 308} 309