1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.qs.touch; 17 18 import static android.view.MotionEvent.INVALID_POINTER_ID; 19 20 import android.content.Context; 21 import android.graphics.PointF; 22 import android.support.annotation.NonNull; 23 import android.support.annotation.VisibleForTesting; 24 import android.util.Log; 25 import android.view.MotionEvent; 26 import android.view.ViewConfiguration; 27 28 /** 29 * One dimensional scroll/drag/swipe gesture detector. 30 * 31 * Definition of swipe is different from android system in that this detector handles 32 * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before 33 * swipe action happens 34 * 35 * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/SwipeDetector.java 36 */ 37 public class SwipeDetector { 38 39 private static final boolean DBG = false; 40 private static final String TAG = "SwipeDetector"; 41 42 private int mScrollConditions; 43 public static final int DIRECTION_POSITIVE = 1 << 0; 44 public static final int DIRECTION_NEGATIVE = 1 << 1; 45 public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE; 46 47 private static final float ANIMATION_DURATION = 1200; 48 49 protected int mActivePointerId = INVALID_POINTER_ID; 50 51 /** 52 * The minimum release velocity in pixels per millisecond that triggers fling.. 53 */ 54 public static final float RELEASE_VELOCITY_PX_MS = 1.0f; 55 56 /** 57 * The time constant used to calculate dampening in the low-pass filter of scroll velocity. 58 * Cutoff frequency is set at 10 Hz. 59 */ 60 public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10); 61 62 /* Scroll state, this is set to true during dragging and animation. */ 63 private ScrollState mState = ScrollState.IDLE; 64 65 enum ScrollState { 66 IDLE, 67 DRAGGING, // onDragStart, onDrag 68 SETTLING // onDragEnd 69 } 70 71 public static abstract class Direction { 72 getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint)73 abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint); 74 75 /** 76 * Distance in pixels a touch can wander before we think the user is scrolling. 77 */ getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos)78 abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos); 79 } 80 81 public static final Direction VERTICAL = new Direction() { 82 83 @Override 84 float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) { 85 return ev.getY(pointerIndex) - refPoint.y; 86 } 87 88 @Override 89 float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { 90 return Math.abs(ev.getX(pointerIndex) - downPos.x); 91 } 92 }; 93 94 public static final Direction HORIZONTAL = new Direction() { 95 96 @Override 97 float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) { 98 return ev.getX(pointerIndex) - refPoint.x; 99 } 100 101 @Override 102 float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) { 103 return Math.abs(ev.getY(pointerIndex) - downPos.y); 104 } 105 }; 106 107 //------------------- ScrollState transition diagram ----------------------------------- 108 // 109 // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING 110 // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING 111 // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING 112 // SETTLING -> (View settled) -> IDLE 113 setState(ScrollState newState)114 private void setState(ScrollState newState) { 115 if (DBG) { 116 Log.d(TAG, "setState:" + mState + "->" + newState); 117 } 118 // onDragStart and onDragEnd is reported ONLY on state transition 119 if (newState == ScrollState.DRAGGING) { 120 initializeDragging(); 121 if (mState == ScrollState.IDLE) { 122 reportDragStart(false /* recatch */); 123 } else if (mState == ScrollState.SETTLING) { 124 reportDragStart(true /* recatch */); 125 } 126 } 127 if (newState == ScrollState.SETTLING) { 128 reportDragEnd(); 129 } 130 131 mState = newState; 132 } 133 isDraggingOrSettling()134 public boolean isDraggingOrSettling() { 135 return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING; 136 } 137 138 /** 139 * There's no touch and there's no animation. 140 */ isIdleState()141 public boolean isIdleState() { 142 return mState == ScrollState.IDLE; 143 } 144 isSettlingState()145 public boolean isSettlingState() { 146 return mState == ScrollState.SETTLING; 147 } 148 isDraggingState()149 public boolean isDraggingState() { 150 return mState == ScrollState.DRAGGING; 151 } 152 153 private final PointF mDownPos = new PointF(); 154 private final PointF mLastPos = new PointF(); 155 private final Direction mDir; 156 157 private final float mTouchSlop; 158 159 /* Client of this gesture detector can register a callback. */ 160 private final Listener mListener; 161 162 private long mCurrentMillis; 163 164 private float mVelocity; 165 private float mLastDisplacement; 166 private float mDisplacement; 167 168 private float mSubtractDisplacement; 169 private boolean mIgnoreSlopWhenSettling; 170 171 public interface Listener { onDragStart(boolean start)172 void onDragStart(boolean start); 173 onDrag(float displacement, float velocity)174 boolean onDrag(float displacement, float velocity); 175 onDragEnd(float velocity, boolean fling)176 void onDragEnd(float velocity, boolean fling); 177 } 178 SwipeDetector(@onNull Context context, @NonNull Listener l, @NonNull Direction dir)179 public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) { 180 this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir); 181 } 182 183 @VisibleForTesting SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir)184 protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) { 185 mTouchSlop = touchSlope; 186 mListener = l; 187 mDir = dir; 188 } 189 setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop)190 public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) { 191 mScrollConditions = scrollDirectionFlags; 192 mIgnoreSlopWhenSettling = ignoreSlop; 193 } 194 shouldScrollStart(MotionEvent ev, int pointerIndex)195 private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) { 196 // reject cases where the angle or slop condition is not met. 197 if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop) 198 > Math.abs(mDisplacement)) { 199 return false; 200 } 201 202 // Check if the client is interested in scroll in current direction. 203 if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) || 204 ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) { 205 return true; 206 } 207 return false; 208 } 209 onTouchEvent(MotionEvent ev)210 public boolean onTouchEvent(MotionEvent ev) { 211 switch (ev.getActionMasked()) { 212 case MotionEvent.ACTION_DOWN: 213 mActivePointerId = ev.getPointerId(0); 214 mDownPos.set(ev.getX(), ev.getY()); 215 mLastPos.set(mDownPos); 216 mLastDisplacement = 0; 217 mDisplacement = 0; 218 mVelocity = 0; 219 220 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 221 setState(ScrollState.DRAGGING); 222 } 223 break; 224 //case MotionEvent.ACTION_POINTER_DOWN: 225 case MotionEvent.ACTION_POINTER_UP: 226 int ptrIdx = ev.getActionIndex(); 227 int ptrId = ev.getPointerId(ptrIdx); 228 if (ptrId == mActivePointerId) { 229 final int newPointerIdx = ptrIdx == 0 ? 1 : 0; 230 mDownPos.set( 231 ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), 232 ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); 233 mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); 234 mActivePointerId = ev.getPointerId(newPointerIdx); 235 } 236 break; 237 case MotionEvent.ACTION_MOVE: 238 int pointerIndex = ev.findPointerIndex(mActivePointerId); 239 if (pointerIndex == INVALID_POINTER_ID) { 240 break; 241 } 242 mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos); 243 computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos), 244 ev.getEventTime()); 245 246 // handle state and listener calls. 247 if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) { 248 setState(ScrollState.DRAGGING); 249 } 250 if (mState == ScrollState.DRAGGING) { 251 reportDragging(); 252 } 253 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); 254 break; 255 case MotionEvent.ACTION_CANCEL: 256 case MotionEvent.ACTION_UP: 257 // These are synthetic events and there is no need to update internal values. 258 if (mState == ScrollState.DRAGGING) { 259 setState(ScrollState.SETTLING); 260 } 261 break; 262 default: 263 break; 264 } 265 return true; 266 } 267 finishedScrolling()268 public void finishedScrolling() { 269 setState(ScrollState.IDLE); 270 } 271 reportDragStart(boolean recatch)272 private boolean reportDragStart(boolean recatch) { 273 mListener.onDragStart(!recatch); 274 if (DBG) { 275 Log.d(TAG, "onDragStart recatch:" + recatch); 276 } 277 return true; 278 } 279 initializeDragging()280 private void initializeDragging() { 281 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { 282 mSubtractDisplacement = 0; 283 } 284 if (mDisplacement > 0) { 285 mSubtractDisplacement = mTouchSlop; 286 } else { 287 mSubtractDisplacement = -mTouchSlop; 288 } 289 } 290 reportDragging()291 private boolean reportDragging() { 292 if (mDisplacement != mLastDisplacement) { 293 if (DBG) { 294 Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f", 295 mDisplacement, mVelocity)); 296 } 297 298 mLastDisplacement = mDisplacement; 299 return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity); 300 } 301 return true; 302 } 303 reportDragEnd()304 private void reportDragEnd() { 305 if (DBG) { 306 Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f", 307 mDisplacement, mVelocity)); 308 } 309 mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS); 310 311 } 312 313 /** 314 * Computes the damped velocity. 315 */ computeVelocity(float delta, long currentMillis)316 public float computeVelocity(float delta, long currentMillis) { 317 long previousMillis = mCurrentMillis; 318 mCurrentMillis = currentMillis; 319 320 float deltaTimeMillis = mCurrentMillis - previousMillis; 321 float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0; 322 if (Math.abs(mVelocity) < 0.001f) { 323 mVelocity = velocity; 324 } else { 325 float alpha = computeDampeningFactor(deltaTimeMillis); 326 mVelocity = interpolate(mVelocity, velocity, alpha); 327 } 328 return mVelocity; 329 } 330 331 /** 332 * Returns a time-dependent dampening factor using delta time. 333 */ computeDampeningFactor(float deltaTime)334 private static float computeDampeningFactor(float deltaTime) { 335 return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime); 336 } 337 338 /** 339 * Returns the linear interpolation between two values 340 */ interpolate(float from, float to, float alpha)341 private static float interpolate(float from, float to, float alpha) { 342 return (1.0f - alpha) * from + alpha * to; 343 } 344 calculateDuration(float velocity, float progressNeeded)345 public static long calculateDuration(float velocity, float progressNeeded) { 346 // TODO: make these values constants after tuning. 347 float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity)); 348 float travelDistance = Math.max(0.2f, progressNeeded); 349 long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); 350 if (DBG) { 351 Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded)); 352 } 353 return duration; 354 } 355 } 356 357