1 /* 2 * Copyright (C) 2016 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 17 package com.android.systemui.pip.phone; 18 19 import android.graphics.PointF; 20 import android.os.Handler; 21 import android.util.Log; 22 import android.view.MotionEvent; 23 import android.view.VelocityTracker; 24 import android.view.ViewConfiguration; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.io.PrintWriter; 29 30 /** 31 * This keeps track of the touch state throughout the current touch gesture. 32 */ 33 public class PipTouchState { 34 private static final String TAG = "PipTouchHandler"; 35 private static final boolean DEBUG = false; 36 37 @VisibleForTesting 38 static final long DOUBLE_TAP_TIMEOUT = 200; 39 40 private final Handler mHandler; 41 private final ViewConfiguration mViewConfig; 42 private final Runnable mDoubleTapTimeoutCallback; 43 44 private VelocityTracker mVelocityTracker; 45 private long mDownTouchTime = 0; 46 private long mLastDownTouchTime = 0; 47 private long mUpTouchTime = 0; 48 private final PointF mDownTouch = new PointF(); 49 private final PointF mDownDelta = new PointF(); 50 private final PointF mLastTouch = new PointF(); 51 private final PointF mLastDelta = new PointF(); 52 private final PointF mVelocity = new PointF(); 53 private boolean mAllowTouches = true; 54 private boolean mIsUserInteracting = false; 55 // Set to true only if the multiple taps occur within the double tap timeout 56 private boolean mIsDoubleTap = false; 57 // Set to true only if a gesture 58 private boolean mIsWaitingForDoubleTap = false; 59 private boolean mIsDragging = false; 60 // The previous gesture was a drag 61 private boolean mPreviouslyDragging = false; 62 private boolean mStartedDragging = false; 63 private boolean mAllowDraggingOffscreen = false; 64 private int mActivePointerId; 65 PipTouchState(ViewConfiguration viewConfig, Handler handler, Runnable doubleTapTimeoutCallback)66 public PipTouchState(ViewConfiguration viewConfig, Handler handler, 67 Runnable doubleTapTimeoutCallback) { 68 mViewConfig = viewConfig; 69 mHandler = handler; 70 mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; 71 } 72 73 /** 74 * Resets this state. 75 */ reset()76 public void reset() { 77 mAllowDraggingOffscreen = false; 78 mIsDragging = false; 79 mStartedDragging = false; 80 mIsUserInteracting = false; 81 } 82 83 /** 84 * Processes a given touch event and updates the state. 85 */ onTouchEvent(MotionEvent ev)86 public void onTouchEvent(MotionEvent ev) { 87 switch (ev.getActionMasked()) { 88 case MotionEvent.ACTION_DOWN: { 89 if (!mAllowTouches) { 90 return; 91 } 92 93 // Initialize the velocity tracker 94 initOrResetVelocityTracker(); 95 addMovement(ev); 96 97 mActivePointerId = ev.getPointerId(0); 98 if (DEBUG) { 99 Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId); 100 } 101 mLastTouch.set(ev.getRawX(), ev.getRawY()); 102 mDownTouch.set(mLastTouch); 103 mAllowDraggingOffscreen = true; 104 mIsUserInteracting = true; 105 mDownTouchTime = ev.getEventTime(); 106 mIsDoubleTap = !mPreviouslyDragging && 107 (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; 108 mIsWaitingForDoubleTap = false; 109 mIsDragging = false; 110 mLastDownTouchTime = mDownTouchTime; 111 if (mDoubleTapTimeoutCallback != null) { 112 mHandler.removeCallbacks(mDoubleTapTimeoutCallback); 113 } 114 break; 115 } 116 case MotionEvent.ACTION_MOVE: { 117 // Skip event if we did not start processing this touch gesture 118 if (!mIsUserInteracting) { 119 break; 120 } 121 122 // Update the velocity tracker 123 addMovement(ev); 124 int pointerIndex = ev.findPointerIndex(mActivePointerId); 125 if (pointerIndex == -1) { 126 Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId); 127 break; 128 } 129 130 float x = ev.getRawX(pointerIndex); 131 float y = ev.getRawY(pointerIndex); 132 mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); 133 mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); 134 135 boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); 136 if (!mIsDragging) { 137 if (hasMovedBeyondTap) { 138 mIsDragging = true; 139 mStartedDragging = true; 140 } 141 } else { 142 mStartedDragging = false; 143 } 144 mLastTouch.set(x, y); 145 break; 146 } 147 case MotionEvent.ACTION_POINTER_UP: { 148 // Skip event if we did not start processing this touch gesture 149 if (!mIsUserInteracting) { 150 break; 151 } 152 153 // Update the velocity tracker 154 addMovement(ev); 155 156 int pointerIndex = ev.getActionIndex(); 157 int pointerId = ev.getPointerId(pointerIndex); 158 if (pointerId == mActivePointerId) { 159 // Select a new active pointer id and reset the movement state 160 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 161 mActivePointerId = ev.getPointerId(newPointerIndex); 162 if (DEBUG) { 163 Log.e(TAG, "Relinquish active pointer id on POINTER_UP: " + 164 mActivePointerId); 165 } 166 mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); 167 } 168 break; 169 } 170 case MotionEvent.ACTION_UP: { 171 // Skip event if we did not start processing this touch gesture 172 if (!mIsUserInteracting) { 173 break; 174 } 175 176 // Update the velocity tracker 177 addMovement(ev); 178 mVelocityTracker.computeCurrentVelocity(1000, 179 mViewConfig.getScaledMaximumFlingVelocity()); 180 mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 181 182 int pointerIndex = ev.findPointerIndex(mActivePointerId); 183 if (pointerIndex == -1) { 184 Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId); 185 break; 186 } 187 188 mUpTouchTime = ev.getEventTime(); 189 mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); 190 mPreviouslyDragging = mIsDragging; 191 mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging && 192 (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; 193 194 // Fall through to clean up 195 } 196 case MotionEvent.ACTION_CANCEL: { 197 recycleVelocityTracker(); 198 break; 199 } 200 } 201 } 202 203 /** 204 * @return the velocity of the active touch pointer at the point it is lifted off the screen. 205 */ 206 public PointF getVelocity() { 207 return mVelocity; 208 } 209 210 /** 211 * @return the last touch position of the active pointer. 212 */ 213 public PointF getLastTouchPosition() { 214 return mLastTouch; 215 } 216 217 /** 218 * @return the movement delta between the last handled touch event and the previous touch 219 * position. 220 */ 221 public PointF getLastTouchDelta() { 222 return mLastDelta; 223 } 224 225 /** 226 * @return the down touch position. 227 */ 228 public PointF getDownTouchPosition() { 229 return mDownTouch; 230 } 231 232 /** 233 * @return the movement delta between the last handled touch event and the down touch 234 * position. 235 */ 236 public PointF getDownTouchDelta() { 237 return mDownDelta; 238 } 239 240 /** 241 * @return whether the user has started dragging. 242 */ 243 public boolean isDragging() { 244 return mIsDragging; 245 } 246 247 /** 248 * @return whether the user is currently interacting with the PiP. 249 */ 250 public boolean isUserInteracting() { 251 return mIsUserInteracting; 252 } 253 254 /** 255 * @return whether the user has started dragging just in the last handled touch event. 256 */ 257 public boolean startedDragging() { 258 return mStartedDragging; 259 } 260 261 /** 262 * Sets whether touching is currently allowed. 263 */ 264 public void setAllowTouches(boolean allowTouches) { 265 mAllowTouches = allowTouches; 266 267 // If the user happens to touch down before this is sent from the system during a transition 268 // then block any additional handling by resetting the state now 269 if (mIsUserInteracting) { 270 reset(); 271 } 272 } 273 274 /** 275 * Disallows dragging offscreen for the duration of the current gesture. 276 */ 277 public void setDisallowDraggingOffscreen() { 278 mAllowDraggingOffscreen = false; 279 } 280 281 /** 282 * @return whether dragging offscreen is allowed during this gesture. 283 */ 284 public boolean allowDraggingOffscreen() { 285 return mAllowDraggingOffscreen; 286 } 287 288 /** 289 * @return whether this gesture is a double-tap. 290 */ 291 public boolean isDoubleTap() { 292 return mIsDoubleTap; 293 } 294 295 /** 296 * @return whether this gesture will potentially lead to a following double-tap. 297 */ 298 public boolean isWaitingForDoubleTap() { 299 return mIsWaitingForDoubleTap; 300 } 301 302 /** 303 * Schedules the callback to run if the next double tap does not occur. Only runs if 304 * isWaitingForDoubleTap() is true. 305 */ 306 public void scheduleDoubleTapTimeoutCallback() { 307 if (mIsWaitingForDoubleTap) { 308 long delay = getDoubleTapTimeoutCallbackDelay(); 309 mHandler.removeCallbacks(mDoubleTapTimeoutCallback); 310 mHandler.postDelayed(mDoubleTapTimeoutCallback, delay); 311 } 312 } 313 314 @VisibleForTesting long getDoubleTapTimeoutCallbackDelay() { 315 if (mIsWaitingForDoubleTap) { 316 return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); 317 } 318 return -1; 319 } 320 321 private void initOrResetVelocityTracker() { 322 if (mVelocityTracker == null) { 323 mVelocityTracker = VelocityTracker.obtain(); 324 } else { 325 mVelocityTracker.clear(); 326 } 327 } 328 329 private void recycleVelocityTracker() { 330 if (mVelocityTracker != null) { 331 mVelocityTracker.recycle(); 332 mVelocityTracker = null; 333 } 334 } 335 336 private void addMovement(MotionEvent event) { 337 // Add movement to velocity tracker using raw screen X and Y coordinates instead 338 // of window coordinates because the window frame may be moving at the same time. 339 float deltaX = event.getRawX() - event.getX(); 340 float deltaY = event.getRawY() - event.getY(); 341 event.offsetLocation(deltaX, deltaY); 342 mVelocityTracker.addMovement(event); 343 event.offsetLocation(-deltaX, -deltaY); 344 } 345 346 public void dump(PrintWriter pw, String prefix) { 347 final String innerPrefix = prefix + " "; 348 pw.println(prefix + TAG); 349 pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); 350 pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); 351 pw.println(innerPrefix + "mDownTouch=" + mDownTouch); 352 pw.println(innerPrefix + "mDownDelta=" + mDownDelta); 353 pw.println(innerPrefix + "mLastTouch=" + mLastTouch); 354 pw.println(innerPrefix + "mLastDelta=" + mLastDelta); 355 pw.println(innerPrefix + "mVelocity=" + mVelocity); 356 pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); 357 pw.println(innerPrefix + "mIsDragging=" + mIsDragging); 358 pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); 359 pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); 360 } 361 } 362