TaskStackViewTouchHandler.java revision 5c9f4b90bf56b242467f0b5b4d2c7c5b71e6a777
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_MOVE: { 166 if (mActivePointerId == INACTIVE_POINTER_ID) break; 167 168 // Initialize the velocity tracker if necessary 169 initVelocityTrackerIfNotExists(); 170 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 171 172 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 173 int y = (int) ev.getY(activePointerIndex); 174 int x = (int) ev.getX(activePointerIndex); 175 if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { 176 // Save the touch move info 177 mIsScrolling = true; 178 // Disallow parents from intercepting touch events 179 final ViewParent parent = mSv.getParent(); 180 if (parent != null) { 181 parent.requestDisallowInterceptTouchEvent(true); 182 } 183 } 184 185 mLastMotionX = x; 186 mLastMotionY = y; 187 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 188 break; 189 } 190 case MotionEvent.ACTION_CANCEL: 191 case MotionEvent.ACTION_UP: { 192 // Animate the scroll back if we've cancelled 193 mScroller.animateBoundScroll(); 194 // Reset the drag state and the velocity tracker 195 mIsScrolling = false; 196 mActivePointerId = INACTIVE_POINTER_ID; 197 mActiveTaskView = null; 198 mTotalPMotion = 0; 199 recycleVelocityTracker(); 200 break; 201 } 202 } 203 204 return wasScrolling || mIsScrolling; 205 } 206 207 /** Handles touch events once we have intercepted them */ 208 public boolean onTouchEvent(MotionEvent ev) { 209 // Short circuit if we have no children 210 boolean hasTaskViews = (mSv.getTaskViews().size() > 0); 211 if (!hasTaskViews) { 212 return false; 213 } 214 215 int action = ev.getAction(); 216 if (mConfig.multiStackEnabled) { 217 // Check if we are within the bounds of the stack view contents 218 if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 219 if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) { 220 return false; 221 } 222 } 223 } 224 225 // Pass through to swipe helper if we are swiping 226 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 227 return true; 228 } 229 230 // Update the velocity tracker 231 initVelocityTrackerIfNotExists(); 232 233 switch (action & MotionEvent.ACTION_MASK) { 234 case MotionEvent.ACTION_DOWN: { 235 // Save the touch down info 236 mInitialMotionX = mLastMotionX = (int) ev.getX(); 237 mInitialMotionY = mLastMotionY = (int) ev.getY(); 238 mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 239 mActivePointerId = ev.getPointerId(0); 240 mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY); 241 // Stop the current scroll if it is still flinging 242 mScroller.stopScroller(); 243 mScroller.stopBoundScrollAnimation(); 244 // Initialize the velocity tracker 245 initOrResetVelocityTracker(); 246 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 247 // Disallow parents from intercepting touch events 248 final ViewParent parent = mSv.getParent(); 249 if (parent != null) { 250 parent.requestDisallowInterceptTouchEvent(true); 251 } 252 break; 253 } 254 case MotionEvent.ACTION_POINTER_DOWN: { 255 final int index = ev.getActionIndex(); 256 mActivePointerId = ev.getPointerId(index); 257 mLastMotionX = (int) ev.getX(index); 258 mLastMotionY = (int) ev.getY(index); 259 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 260 break; 261 } 262 case MotionEvent.ACTION_MOVE: { 263 if (mActivePointerId == INACTIVE_POINTER_ID) break; 264 265 mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); 266 267 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 268 int x = (int) ev.getX(activePointerIndex); 269 int y = (int) ev.getY(activePointerIndex); 270 int yTotal = Math.abs(y - mInitialMotionY); 271 float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y); 272 float deltaP = mLastP - curP; 273 if (!mIsScrolling) { 274 if (yTotal > mScrollTouchSlop) { 275 mIsScrolling = true; 276 // Disallow parents from intercepting touch events 277 final ViewParent parent = mSv.getParent(); 278 if (parent != null) { 279 parent.requestDisallowInterceptTouchEvent(true); 280 } 281 } 282 } 283 if (mIsScrolling) { 284 float curStackScroll = mScroller.getStackScroll(); 285 float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP); 286 if (Float.compare(overScrollAmount, 0f) != 0) { 287 // Bound the overscroll to a fixed amount, and inversely scale the y-movement 288 // relative to how close we are to the max overscroll 289 float maxOverScroll = mConfig.taskStackOverscrollPct; 290 deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount) 291 / maxOverScroll)); 292 } 293 mScroller.setStackScroll(curStackScroll + deltaP); 294 } 295 mLastMotionX = x; 296 mLastMotionY = y; 297 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 298 mTotalPMotion += Math.abs(deltaP); 299 break; 300 } 301 case MotionEvent.ACTION_UP: { 302 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 303 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 304 if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { 305 float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); 306 int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * 307 (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange - 308 Constants.Values.TaskStackView.TaskStackMinOverscrollRange)); 309 mScroller.mScroller.fling(0, 310 mScroller.progressToScrollRange(mScroller.getStackScroll()), 311 0, velocity, 312 0, 0, 313 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP), 314 mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP), 315 0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange + 316 overscrollRange); 317 // Invalidate to kick off computeScroll 318 mSv.invalidate(); 319 } else if (mScroller.isScrollOutOfBounds()) { 320 // Animate the scroll back into bounds 321 mScroller.animateBoundScroll(); 322 } else if (mActiveTaskView == null) { 323 // This tap didn't start on a task. 324 maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); 325 } 326 327 mActivePointerId = INACTIVE_POINTER_ID; 328 mIsScrolling = false; 329 mTotalPMotion = 0; 330 recycleVelocityTracker(); 331 break; 332 } 333 case MotionEvent.ACTION_POINTER_UP: { 334 int pointerIndex = ev.getActionIndex(); 335 int pointerId = ev.getPointerId(pointerIndex); 336 if (pointerId == mActivePointerId) { 337 // Select a new active pointer id and reset the motion state 338 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 339 mActivePointerId = ev.getPointerId(newPointerIndex); 340 mLastMotionX = (int) ev.getX(newPointerIndex); 341 mLastMotionY = (int) ev.getY(newPointerIndex); 342 mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY); 343 mVelocityTracker.clear(); 344 } 345 break; 346 } 347 case MotionEvent.ACTION_CANCEL: { 348 if (mScroller.isScrollOutOfBounds()) { 349 // Animate the scroll back into bounds 350 mScroller.animateBoundScroll(); 351 } 352 mActivePointerId = INACTIVE_POINTER_ID; 353 mIsScrolling = false; 354 mTotalPMotion = 0; 355 recycleVelocityTracker(); 356 break; 357 } 358 } 359 return true; 360 } 361 362 /** Hides recents if the up event at (x, y) is a tap on the background area. */ 363 void maybeHideRecentsFromBackgroundTap(int x, int y) { 364 // Ignore the up event if it's too far from its start position. The user might have been 365 // trying to scroll or swipe. 366 int dx = Math.abs(mInitialMotionX - x); 367 int dy = Math.abs(mInitialMotionY - y); 368 if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { 369 return; 370 } 371 372 // Shift the tap position toward the center of the task stack and check to see if it would 373 // have hit a view. The user might have tried to tap on a task and missed slightly. 374 int shiftedX = x; 375 if (x > mSv.getTouchableRegion().centerX()) { 376 shiftedX -= mWindowTouchSlop; 377 } else { 378 shiftedX += mWindowTouchSlop; 379 } 380 if (findViewAtPoint(shiftedX, y) != null) { 381 return; 382 } 383 384 // The user intentionally tapped on the background, which is like a tap on the "desktop". 385 // Hide recents and transition to the launcher. 386 Recents recents = Recents.getInstanceAndStartIfNeeded(mSv.getContext()); 387 recents.hideRecents(false /* altTab */, true /* homeKey */); 388 } 389 390 /** Handles generic motion events */ 391 public boolean onGenericMotionEvent(MotionEvent ev) { 392 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 393 InputDevice.SOURCE_CLASS_POINTER) { 394 int action = ev.getAction(); 395 switch (action & MotionEvent.ACTION_MASK) { 396 case MotionEvent.ACTION_SCROLL: 397 // Find the front most task and scroll the next task to the front 398 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 399 if (vScroll > 0) { 400 if (mSv.ensureFocusedTask(true)) { 401 mSv.focusNextTask(true, false); 402 } 403 } else { 404 if (mSv.ensureFocusedTask(true)) { 405 mSv.focusNextTask(false, false); 406 } 407 } 408 return true; 409 } 410 } 411 return false; 412 } 413 414 /**** SwipeHelper Implementation ****/ 415 416 @Override 417 public View getChildAtPosition(MotionEvent ev) { 418 return findViewAtPoint((int) ev.getX(), (int) ev.getY()); 419 } 420 421 @Override 422 public boolean canChildBeDismissed(View v) { 423 return true; 424 } 425 426 @Override 427 public void onBeginDrag(View v) { 428 TaskView tv = (TaskView) v; 429 // Disable clipping with the stack while we are swiping 430 tv.setClipViewInStack(false); 431 // Disallow touch events from this task view 432 tv.setTouchEnabled(false); 433 // Disallow parents from intercepting touch events 434 final ViewParent parent = mSv.getParent(); 435 if (parent != null) { 436 parent.requestDisallowInterceptTouchEvent(true); 437 } 438 // Fade out the dismiss button 439 mSv.hideDismissAllButton(null); 440 } 441 442 @Override 443 public void onSwipeChanged(View v, float delta) { 444 // Do nothing 445 } 446 447 @Override 448 public void onChildDismissed(View v) { 449 TaskView tv = (TaskView) v; 450 // Re-enable clipping with the stack (we will reuse this view) 451 tv.setClipViewInStack(true); 452 // Re-enable touch events from this task view 453 tv.setTouchEnabled(true); 454 // Remove the task view from the stack 455 mSv.onTaskViewDismissed(tv); 456 // Keep track of deletions by keyboard 457 MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", 458 Constants.Metrics.DismissSourceSwipeGesture); 459 } 460 461 @Override 462 public void onSnapBackCompleted(View v) { 463 TaskView tv = (TaskView) v; 464 // Re-enable clipping with the stack 465 tv.setClipViewInStack(true); 466 // Re-enable touch events from this task view 467 tv.setTouchEnabled(true); 468 // Restore the dismiss button 469 mSv.showDismissAllButton(); 470 } 471 472 @Override 473 public void onDragCancelled(View v) { 474 // Do nothing 475 } 476} 477