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