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