1/* 2 * Copyright (C) 2014 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 17package com.android.systemui.recents.views; 18 19import android.content.Context; 20import android.view.InputDevice; 21import android.view.MotionEvent; 22import android.view.VelocityTracker; 23import android.view.View; 24import android.view.ViewConfiguration; 25import android.view.ViewParent; 26import com.android.internal.logging.MetricsLogger; 27import com.android.systemui.recents.Constants; 28import com.android.systemui.recents.Recents; 29import com.android.systemui.recents.RecentsConfiguration; 30 31import java.util.List; 32 33/* Handles touch events for a TaskStackView. */ 34class TaskStackViewTouchHandler implements SwipeHelper.Callback { 35 static int INACTIVE_POINTER_ID = -1; 36 37 RecentsConfiguration mConfig; 38 TaskStackView mSv; 39 TaskStackViewScroller mScroller; 40 VelocityTracker mVelocityTracker; 41 42 boolean mIsScrolling; 43 44 float mInitialP; 45 float mLastP; 46 float mTotalPMotion; 47 int mInitialMotionX, mInitialMotionY; 48 int mLastMotionX, mLastMotionY; 49 int mActivePointerId = INACTIVE_POINTER_ID; 50 TaskView mActiveTaskView = null; 51 52 int mMinimumVelocity; 53 int mMaximumVelocity; 54 // The scroll touch slop is used to calculate when we start scrolling 55 int mScrollTouchSlop; 56 // The page touch slop is used to calculate when we start swiping 57 float mPagingTouchSlop; 58 // Used to calculate when a tap is outside a task view rectangle. 59 final int mWindowTouchSlop; 60 61 SwipeHelper mSwipeHelper; 62 boolean mInterceptedBySwipeHelper; 63 64 public TaskStackViewTouchHandler(Context context, TaskStackView sv, 65 RecentsConfiguration config, TaskStackViewScroller scroller) { 66 ViewConfiguration configuration = ViewConfiguration.get(context); 67 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 68 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 69 mScrollTouchSlop = configuration.getScaledTouchSlop(); 70 mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); 71 mWindowTouchSlop = configuration.getScaledWindowTouchSlop(); 72 mSv = sv; 73 mScroller = scroller; 74 mConfig = config; 75 76 float densityScale = context.getResources().getDisplayMetrics().density; 77 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop); 78 mSwipeHelper.setMinAlpha(1f); 79 } 80 81 /** Velocity tracker helpers */ 82 void initOrResetVelocityTracker() { 83 if (mVelocityTracker == null) { 84 mVelocityTracker = VelocityTracker.obtain(); 85 } else { 86 mVelocityTracker.clear(); 87 } 88 } 89 void initVelocityTrackerIfNotExists() { 90 if (mVelocityTracker == null) { 91 mVelocityTracker = VelocityTracker.obtain(); 92 } 93 } 94 void recycleVelocityTracker() { 95 if (mVelocityTracker != null) { 96 mVelocityTracker.recycle(); 97 mVelocityTracker = null; 98 } 99 } 100 101 /** Returns the view at the specified coordinates */ 102 TaskView findViewAtPoint(int x, int y) { 103 List<TaskView> taskViews = mSv.getTaskViews(); 104 int taskViewCount = taskViews.size(); 105 for (int i = taskViewCount - 1; i >= 0; i--) { 106 TaskView tv = taskViews.get(i); 107 if (tv.getVisibility() == View.VISIBLE) { 108 if (mSv.isTransformedTouchPointInView(x, y, tv)) { 109 return tv; 110 } 111 } 112 } 113 return null; 114 } 115 116 /** Constructs a simulated motion event for the current stack scroll. */ 117 MotionEvent createMotionEventForStackScroll(MotionEvent ev) { 118 MotionEvent pev = MotionEvent.obtainNoHistory(ev); 119 pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll())); 120 return pev; 121 } 122 123 /** Touch preprocessing for handling below */ 124 public boolean onInterceptTouchEvent(MotionEvent ev) { 125 // Return early if we have no children 126 boolean hasTaskViews = (mSv.getTaskViews().size() > 0); 127 if (!hasTaskViews) { 128 return false; 129 } 130 131 int action = ev.getAction(); 132 if (mConfig.multiStackEnabled) { 133 // Check if we are within the bounds of the stack view contents 134 if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 135 if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) { 136 return false; 137 } 138 } 139 } 140 141 // Pass through to swipe helper if we are swiping 142 mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); 143 if (mInterceptedBySwipeHelper) { 144 return true; 145 } 146 147 boolean wasScrolling = mScroller.isScrolling() || 148 (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning()); 149 switch (action & MotionEvent.ACTION_MASK) { 150 case MotionEvent.ACTION_DOWN: { 151 // Save the touch down info 152 mInitialMotionX = mLastMotionX = (int) ev.getX(); 153 mInitialMotionY = mLastMotionY = (int) ev.getY(); 154 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 155 mActivePointerId = ev.getPointerId(0); 156 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 157 // Stop the current scroll if it is still flinging 158 mScroller.stopScroller(); 159 mScroller.stopBoundScrollAnimation(); 160 // Initialize the velocity tracker 161 initOrResetVelocityTracker(); 162 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 163 break; 164 } 165 case MotionEvent.ACTION_POINTER_DOWN: { 166 final int index = ev.getActionIndex(); 167 mActivePointerId = ev.getPointerId(index); 168 mLastMotionX = (int) ev.getX(index); 169 mLastMotionY = (int) ev.getY(index); 170 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 171 break; 172 } 173 case MotionEvent.ACTION_MOVE: { 174 if (mActivePointerId == INACTIVE_POINTER_ID) break; 175 176 // Initialize the velocity tracker if necessary 177 initVelocityTrackerIfNotExists(); 178 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 179 180 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 181 int y = (int) ev.getY(activePointerIndex); 182 int x = (int) ev.getX(activePointerIndex); 183 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 184 // Save the touch move info 185 mIsScrolling = true; 186 // Disallow parents from intercepting touch events 187 final ViewParent parent = mSv.getParent(); 188 if (parent != null) { 189 parent.requestDisallowInterceptTouchEvent(true); 190 } 191 } 192 193 mLastMotionX = x; 194 mLastMotionY = y; 195 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 196 break; 197 } 198 case MotionEvent.ACTION_POINTER_UP: { 199 int pointerIndex = ev.getActionIndex(); 200 int pointerId = ev.getPointerId(pointerIndex); 201 if (pointerId == mActivePointerId) { 202 // Select a new active pointer id and reset the motion state 203 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 204 mActivePointerId = ev.getPointerId(newPointerIndex); 205 mLastMotionX = (int) ev.getX(newPointerIndex); 206 mLastMotionY = (int) ev.getY(newPointerIndex); 207 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 208 mVelocityTracker.clear(); 209 } 210 break; 211 } 212 case MotionEvent.ACTION_CANCEL: 213 case MotionEvent.ACTION_UP: { 214 // Animate the scroll back if we've cancelled 215 mScroller.animateBoundScroll(); 216 // Reset the drag state and the velocity tracker 217 mIsScrolling = false; 218 mActivePointerId = INACTIVE_POINTER_ID; 219 mActiveTaskView = null; 220 mTotalPMotion = 0; 221 recycleVelocityTracker(); 222 break; 223 } 224 } 225 226 return wasScrolling || mIsScrolling; 227 } 228 229 /** Handles touch events once we have intercepted them */ 230 public boolean onTouchEvent(MotionEvent ev) { 231 // Short circuit if we have no children 232 boolean hasTaskViews = (mSv.getTaskViews().size() > 0); 233 if (!hasTaskViews) { 234 return false; 235 } 236 237 int action = ev.getAction(); 238 if (mConfig.multiStackEnabled) { 239 // Check if we are within the bounds of the stack view contents 240 if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 241 if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) { 242 return false; 243 } 244 } 245 } 246 247 // Pass through to swipe helper if we are swiping 248 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 249 return true; 250 } 251 252 // Update the velocity tracker 253 initVelocityTrackerIfNotExists(); 254 255 switch (action & MotionEvent.ACTION_MASK) { 256 case MotionEvent.ACTION_DOWN: { 257 // Save the touch down info 258 mInitialMotionX = mLastMotionX = (int) ev.getX(); 259 mInitialMotionY = mLastMotionY = (int) ev.getY(); 260 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 261 mActivePointerId = ev.getPointerId(0); 262 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 263 // Stop the current scroll if it is still flinging 264 mScroller.stopScroller(); 265 mScroller.stopBoundScrollAnimation(); 266 // Initialize the velocity tracker 267 initOrResetVelocityTracker(); 268 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 269 // Disallow parents from intercepting touch events 270 final ViewParent parent = mSv.getParent(); 271 if (parent != null) { 272 parent.requestDisallowInterceptTouchEvent(true); 273 } 274 break; 275 } 276 case MotionEvent.ACTION_POINTER_DOWN: { 277 final int index = ev.getActionIndex(); 278 mActivePointerId = ev.getPointerId(index); 279 mLastMotionX = (int) ev.getX(index); 280 mLastMotionY = (int) ev.getY(index); 281 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 282 break; 283 } 284 case MotionEvent.ACTION_MOVE: { 285 if (mActivePointerId == INACTIVE_POINTER_ID) break; 286 287 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 288 289 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 290 int x = (int) ev.getX(activePointerIndex); 291 int y = (int) ev.getY(activePointerIndex); 292 int yTotal = Math.abs(y - mInitialMotionY); 293 float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y); 294 float deltaP = mLastP - curP; 295 if (!mIsScrolling) { 296 if (yTotal > mScrollTouchSlop) { 297 mIsScrolling = true; 298 // Disallow parents from intercepting touch events 299 final ViewParent parent = mSv.getParent(); 300 if (parent != null) { 301 parent.requestDisallowInterceptTouchEvent(true); 302 } 303 } 304 } 305 if (mIsScrolling) { 306 float curStackScroll = mScroller.getStackScroll(); 307 float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP); 308 if (Float.compare(overScrollAmount, 0f) != 0) { 309 // Bound the overscroll to a fixed amount, and inversely scale the y-movement 310 // relative to how close we are to the max overscroll 311 float maxOverScroll = mConfig.taskStackOverscrollPct; 312 deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount) 313 / maxOverScroll)); 314 } 315 mScroller.setStackScroll(curStackScroll + deltaP); 316 } 317 mLastMotionX = x; 318 mLastMotionY = y; 319 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 320 mTotalPMotion += Math.abs(deltaP); 321 break; 322 } 323 case MotionEvent.ACTION_UP: { 324 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 325 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 326 if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { 327 float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); 328 int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * 329 (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange - 330 Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); 331 mScroller.mScroller.fling(0, 332 mScroller.progressToScrollRange(mScroller.getStackScroll()), 333 0, velocity, 334 0, 0, 335 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP), 336 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP), 337 0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange + 338 overscrollRange); 339 // Invalidate to kick off computeScroll 340 mSv.invalidate(); 341 } else if (mIsScrolling && mScroller.isScrollOutOfBounds()) { 342 // Animate the scroll back into bounds 343 mScroller.animateBoundScroll(); 344 } else if (mActiveTaskView == null) { 345 // This tap didn't start on a task. 346 maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); 347 } 348 349 mActivePointerId = INACTIVE_POINTER_ID; 350 mIsScrolling = false; 351 mTotalPMotion = 0; 352 recycleVelocityTracker(); 353 break; 354 } 355 case MotionEvent.ACTION_POINTER_UP: { 356 int pointerIndex = ev.getActionIndex(); 357 int pointerId = ev.getPointerId(pointerIndex); 358 if (pointerId == mActivePointerId) { 359 // Select a new active pointer id and reset the motion state 360 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 361 mActivePointerId = ev.getPointerId(newPointerIndex); 362 mLastMotionX = (int) ev.getX(newPointerIndex); 363 mLastMotionY = (int) ev.getY(newPointerIndex); 364 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 365 mVelocityTracker.clear(); 366 } 367 break; 368 } 369 case MotionEvent.ACTION_CANCEL: { 370 if (mScroller.isScrollOutOfBounds()) { 371 // Animate the scroll back into bounds 372 mScroller.animateBoundScroll(); 373 } 374 mActivePointerId = INACTIVE_POINTER_ID; 375 mIsScrolling = false; 376 mTotalPMotion = 0; 377 recycleVelocityTracker(); 378 break; 379 } 380 } 381 return true; 382 } 383 384 /** Hides recents if the up event at (x, y) is a tap on the background area. */ 385 void maybeHideRecentsFromBackgroundTap(int x, int y) { 386 // Ignore the up event if it's too far from its start position. The user might have been 387 // trying to scroll or swipe. 388 int dx = Math.abs(mInitialMotionX - x); 389 int dy = Math.abs(mInitialMotionY - y); 390 if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { 391 return; 392 } 393 394 // Shift the tap position toward the center of the task stack and check to see if it would 395 // have hit a view. The user might have tried to tap on a task and missed slightly. 396 int shiftedX = x; 397 if (x > mSv.getTouchableRegion().centerX()) { 398 shiftedX -= mWindowTouchSlop; 399 } else { 400 shiftedX += mWindowTouchSlop; 401 } 402 if (findViewAtPoint(shiftedX, y) != null) { 403 return; 404 } 405 406 // The user intentionally tapped on the background, which is like a tap on the "desktop". 407 // Hide recents and transition to the launcher. 408 Recents recents = Recents.getInstanceAndStartIfNeeded(mSv.getContext()); 409 recents.hideRecents(false /* altTab */, true /* homeKey */); 410 } 411 412 /** Handles generic motion events */ 413 public boolean onGenericMotionEvent(MotionEvent ev) { 414 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 415 InputDevice.SOURCE_CLASS_POINTER) { 416 int action = ev.getAction(); 417 switch (action & MotionEvent.ACTION_MASK) { 418 case MotionEvent.ACTION_SCROLL: 419 // Find the front most task and scroll the next task to the front 420 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 421 if (vScroll > 0) { 422 if (mSv.ensureFocusedTask(true)) { 423 mSv.focusNextTask(true, false); 424 } 425 } else { 426 if (mSv.ensureFocusedTask(true)) { 427 mSv.focusNextTask(false, false); 428 } 429 } 430 return true; 431 } 432 } 433 return false; 434 } 435 436 /**** SwipeHelper Implementation ****/ 437 438 @Override 439 public View getChildAtPosition(MotionEvent ev) { 440 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 441 } 442 443 @Override 444 public boolean canChildBeDismissed(View v) { 445 return true; 446 } 447 448 @Override 449 public void onBeginDrag(View v) { 450 TaskView tv = (TaskView) v; 451 // Disable clipping with the stack while we are swiping 452 tv.setClipViewInStack(false); 453 // Disallow touch events from this task view 454 tv.setTouchEnabled(false); 455 // Disallow parents from intercepting touch events 456 final ViewParent parent = mSv.getParent(); 457 if (parent != null) { 458 parent.requestDisallowInterceptTouchEvent(true); 459 } 460 // Fade out the dismiss button 461 mSv.hideDismissAllButton(null); 462 } 463 464 @Override 465 public void onSwipeChanged(View v, float delta) { 466 // Do nothing 467 } 468 469 @Override 470 public void onChildDismissed(View v) { 471 TaskView tv = (TaskView) v; 472 // Re-enable clipping with the stack (we will reuse this view) 473 tv.setClipViewInStack(true); 474 // Re-enable touch events from this task view 475 tv.setTouchEnabled(true); 476 // Remove the task view from the stack 477 mSv.onTaskViewDismissed(tv); 478 // Keep track of deletions by keyboard 479 MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", 480 Constants.Metrics.DismissSourceSwipeGesture); 481 } 482 483 @Override 484 public void onSnapBackCompleted(View v) { 485 TaskView tv = (TaskView) v; 486 // Re-enable clipping with the stack 487 tv.setClipViewInStack(true); 488 // Re-enable touch events from this task view 489 tv.setTouchEnabled(true); 490 // Restore the dismiss button 491 mSv.showDismissAllButton(); 492 } 493 494 @Override 495 public void onDragCancelled(View v) { 496 // Do nothing 497 } 498} 499