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 17 package com.android.systemui.recents.views; 18 19 import android.animation.Animator; 20 import android.animation.ValueAnimator; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Path; 24 import android.graphics.Rect; 25 import android.util.ArrayMap; 26 import android.util.MutableBoolean; 27 import android.view.InputDevice; 28 import android.view.MotionEvent; 29 import android.view.VelocityTracker; 30 import android.view.View; 31 import android.view.ViewConfiguration; 32 import android.view.ViewDebug; 33 import android.view.ViewParent; 34 import android.view.animation.Interpolator; 35 36 import com.android.internal.logging.MetricsLogger; 37 import com.android.internal.logging.MetricsProto.MetricsEvent; 38 import com.android.systemui.Interpolators; 39 import com.android.systemui.R; 40 import com.android.systemui.SwipeHelper; 41 import com.android.systemui.recents.Constants; 42 import com.android.systemui.recents.Recents; 43 import com.android.systemui.recents.events.EventBus; 44 import com.android.systemui.recents.events.activity.HideRecentsEvent; 45 import com.android.systemui.recents.events.ui.StackViewScrolledEvent; 46 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; 47 import com.android.systemui.recents.misc.FreePathInterpolator; 48 import com.android.systemui.recents.misc.SystemServicesProxy; 49 import com.android.systemui.recents.misc.Utilities; 50 import com.android.systemui.recents.model.Task; 51 import com.android.systemui.statusbar.FlingAnimationUtils; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 56 /** 57 * Handles touch events for a TaskStackView. 58 */ 59 class TaskStackViewTouchHandler implements SwipeHelper.Callback { 60 61 private static final int INACTIVE_POINTER_ID = -1; 62 private static final float CHALLENGING_SWIPE_ESCAPE_VELOCITY = 800f; // dp/sec 63 // The min overscroll is the amount of task progress overscroll we want / the max overscroll 64 // curve value below 65 private static final float MAX_OVERSCROLL = 0.7f / 0.3f; 66 private static final Interpolator OVERSCROLL_INTERP; 67 static { 68 Path OVERSCROLL_PATH = new Path(); 69 OVERSCROLL_PATH.moveTo(0, 0); 70 OVERSCROLL_PATH.cubicTo(0.2f, 0.175f, 0.25f, 0.3f, 1f, 0.3f); 71 OVERSCROLL_INTERP = new FreePathInterpolator(OVERSCROLL_PATH); 72 } 73 74 Context mContext; 75 TaskStackView mSv; 76 TaskStackViewScroller mScroller; 77 VelocityTracker mVelocityTracker; 78 FlingAnimationUtils mFlingAnimUtils; 79 ValueAnimator mScrollFlingAnimator; 80 81 @ViewDebug.ExportedProperty(category="recents") 82 boolean mIsScrolling; 83 float mDownScrollP; 84 int mDownX, mDownY; 85 int mLastY; 86 int mActivePointerId = INACTIVE_POINTER_ID; 87 int mOverscrollSize; 88 TaskView mActiveTaskView = null; 89 90 int mMinimumVelocity; 91 int mMaximumVelocity; 92 // The scroll touch slop is used to calculate when we start scrolling 93 int mScrollTouchSlop; 94 // Used to calculate when a tap is outside a task view rectangle. 95 final int mWindowTouchSlop; 96 97 private final StackViewScrolledEvent mStackViewScrolledEvent = new StackViewScrolledEvent(); 98 99 // The current and final set of task transforms, sized to match the list of tasks in the stack 100 private ArrayList<Task> mCurrentTasks = new ArrayList<>(); 101 private ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>(); 102 private ArrayList<TaskViewTransform> mFinalTaskTransforms = new ArrayList<>(); 103 private ArrayMap<View, Animator> mSwipeHelperAnimations = new ArrayMap<>(); 104 private TaskViewTransform mTmpTransform = new TaskViewTransform(); 105 private float mTargetStackScroll; 106 107 SwipeHelper mSwipeHelper; 108 boolean mInterceptedBySwipeHelper; 109 TaskStackViewTouchHandler(Context context, TaskStackView sv, TaskStackViewScroller scroller)110 public TaskStackViewTouchHandler(Context context, TaskStackView sv, 111 TaskStackViewScroller scroller) { 112 Resources res = context.getResources(); 113 ViewConfiguration configuration = ViewConfiguration.get(context); 114 mContext = context; 115 mSv = sv; 116 mScroller = scroller; 117 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 118 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 119 mScrollTouchSlop = configuration.getScaledTouchSlop(); 120 mWindowTouchSlop = configuration.getScaledWindowTouchSlop(); 121 mFlingAnimUtils = new FlingAnimationUtils(context, 0.2f); 122 mOverscrollSize = res.getDimensionPixelSize(R.dimen.recents_fling_overscroll_distance); 123 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, context) { 124 @Override 125 protected float getSize(View v) { 126 return getScaledDismissSize(); 127 } 128 129 @Override 130 protected void prepareDismissAnimation(View v, Animator anim) { 131 mSwipeHelperAnimations.put(v, anim); 132 } 133 134 @Override 135 protected void prepareSnapBackAnimation(View v, Animator anim) { 136 anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 137 mSwipeHelperAnimations.put(v, anim); 138 } 139 140 @Override 141 protected float getUnscaledEscapeVelocity() { 142 return CHALLENGING_SWIPE_ESCAPE_VELOCITY; 143 } 144 145 @Override 146 protected long getMaxEscapeAnimDuration() { 147 return 700; 148 } 149 }; 150 mSwipeHelper.setDisableHardwareLayers(true); 151 } 152 153 /** Velocity tracker helpers */ initOrResetVelocityTracker()154 void initOrResetVelocityTracker() { 155 if (mVelocityTracker == null) { 156 mVelocityTracker = VelocityTracker.obtain(); 157 } else { 158 mVelocityTracker.clear(); 159 } 160 } recycleVelocityTracker()161 void recycleVelocityTracker() { 162 if (mVelocityTracker != null) { 163 mVelocityTracker.recycle(); 164 mVelocityTracker = null; 165 } 166 } 167 168 /** Touch preprocessing for handling below */ onInterceptTouchEvent(MotionEvent ev)169 public boolean onInterceptTouchEvent(MotionEvent ev) { 170 // Pass through to swipe helper if we are swiping 171 mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); 172 if (mInterceptedBySwipeHelper) { 173 return true; 174 } 175 176 return handleTouchEvent(ev); 177 } 178 179 /** Handles touch events once we have intercepted them */ onTouchEvent(MotionEvent ev)180 public boolean onTouchEvent(MotionEvent ev) { 181 // Pass through to swipe helper if we are swiping 182 if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { 183 return true; 184 } 185 186 handleTouchEvent(ev); 187 return true; 188 } 189 190 /** 191 * Finishes all scroll-fling and non-dismissing animations currently running. 192 */ cancelNonDismissTaskAnimations()193 public void cancelNonDismissTaskAnimations() { 194 Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator); 195 if (!mSwipeHelperAnimations.isEmpty()) { 196 // For the non-dismissing tasks, freeze the position into the task overrides 197 List<TaskView> taskViews = mSv.getTaskViews(); 198 for (int i = taskViews.size() - 1; i >= 0; i--) { 199 TaskView tv = taskViews.get(i); 200 201 if (mSv.isIgnoredTask(tv.getTask())) { 202 continue; 203 } 204 205 tv.cancelTransformAnimation(); 206 mSv.getStackAlgorithm().addUnfocusedTaskOverride(tv, mTargetStackScroll); 207 } 208 mSv.getStackAlgorithm().setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); 209 // Update the scroll to the final scroll position from onBeginDrag() 210 mSv.getScroller().setStackScroll(mTargetStackScroll, null); 211 212 mSwipeHelperAnimations.clear(); 213 } 214 mActiveTaskView = null; 215 } 216 handleTouchEvent(MotionEvent ev)217 private boolean handleTouchEvent(MotionEvent ev) { 218 // Short circuit if we have no children 219 if (mSv.getTaskViews().size() == 0) { 220 return false; 221 } 222 223 final TaskStackLayoutAlgorithm layoutAlgorithm = mSv.mLayoutAlgorithm; 224 int action = ev.getAction(); 225 switch (action & MotionEvent.ACTION_MASK) { 226 case MotionEvent.ACTION_DOWN: { 227 // Stop the current scroll if it is still flinging 228 mScroller.stopScroller(); 229 mScroller.stopBoundScrollAnimation(); 230 mScroller.resetDeltaScroll(); 231 cancelNonDismissTaskAnimations(); 232 mSv.cancelDeferredTaskViewLayoutAnimation(); 233 234 // Save the touch down info 235 mDownX = (int) ev.getX(); 236 mDownY = (int) ev.getY(); 237 mLastY = mDownY; 238 mDownScrollP = mScroller.getStackScroll(); 239 mActivePointerId = ev.getPointerId(0); 240 mActiveTaskView = findViewAtPoint(mDownX, mDownY); 241 242 // Initialize the velocity tracker 243 initOrResetVelocityTracker(); 244 mVelocityTracker.addMovement(ev); 245 break; 246 } 247 case MotionEvent.ACTION_POINTER_DOWN: { 248 final int index = ev.getActionIndex(); 249 mActivePointerId = ev.getPointerId(index); 250 mDownX = (int) ev.getX(index); 251 mDownY = (int) ev.getY(index); 252 mLastY = mDownY; 253 mDownScrollP = mScroller.getStackScroll(); 254 mScroller.resetDeltaScroll(); 255 mVelocityTracker.addMovement(ev); 256 break; 257 } 258 case MotionEvent.ACTION_MOVE: { 259 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 260 int y = (int) ev.getY(activePointerIndex); 261 int x = (int) ev.getX(activePointerIndex); 262 if (!mIsScrolling) { 263 int yDiff = Math.abs(y - mDownY); 264 int xDiff = Math.abs(x - mDownX); 265 if (Math.abs(y - mDownY) > mScrollTouchSlop && yDiff > xDiff) { 266 mIsScrolling = true; 267 float stackScroll = mScroller.getStackScroll(); 268 List<TaskView> taskViews = mSv.getTaskViews(); 269 for (int i = taskViews.size() - 1; i >= 0; i--) { 270 layoutAlgorithm.addUnfocusedTaskOverride(taskViews.get(i).getTask(), 271 stackScroll); 272 } 273 layoutAlgorithm.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); 274 275 // Disallow parents from intercepting touch events 276 final ViewParent parent = mSv.getParent(); 277 if (parent != null) { 278 parent.requestDisallowInterceptTouchEvent(true); 279 } 280 281 MetricsLogger.action(mSv.getContext(), MetricsEvent.OVERVIEW_SCROLL); 282 } 283 } 284 if (mIsScrolling) { 285 // If we just move linearly on the screen, then that would map to 1/arclength 286 // of the curve, so just move the scroll proportional to that 287 float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y); 288 289 // Modulate the overscroll to prevent users from pulling the stack too far 290 float minScrollP = layoutAlgorithm.mMinScrollP; 291 float maxScrollP = layoutAlgorithm.mMaxScrollP; 292 float curScrollP = mDownScrollP + deltaP; 293 if (curScrollP < minScrollP || curScrollP > maxScrollP) { 294 float clampedScrollP = Utilities.clamp(curScrollP, minScrollP, maxScrollP); 295 float overscrollP = (curScrollP - clampedScrollP); 296 float overscrollX = Math.abs(overscrollP) / MAX_OVERSCROLL; 297 float interpX = OVERSCROLL_INTERP.getInterpolation(overscrollX); 298 curScrollP = clampedScrollP + Math.signum(overscrollP) * 299 (interpX * MAX_OVERSCROLL); 300 } 301 mDownScrollP += mScroller.setDeltaStackScroll(mDownScrollP, 302 curScrollP - mDownScrollP); 303 mStackViewScrolledEvent.updateY(y - mLastY); 304 EventBus.getDefault().send(mStackViewScrolledEvent); 305 } 306 307 mLastY = y; 308 mVelocityTracker.addMovement(ev); 309 break; 310 } 311 case MotionEvent.ACTION_POINTER_UP: { 312 int pointerIndex = ev.getActionIndex(); 313 int pointerId = ev.getPointerId(pointerIndex); 314 if (pointerId == mActivePointerId) { 315 // Select a new active pointer id and reset the motion state 316 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 317 mActivePointerId = ev.getPointerId(newPointerIndex); 318 mDownX = (int) ev.getX(pointerIndex); 319 mDownY = (int) ev.getY(pointerIndex); 320 mLastY = mDownY; 321 mDownScrollP = mScroller.getStackScroll(); 322 } 323 mVelocityTracker.addMovement(ev); 324 break; 325 } 326 case MotionEvent.ACTION_UP: { 327 mVelocityTracker.addMovement(ev); 328 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 329 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 330 int y = (int) ev.getY(activePointerIndex); 331 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 332 if (mIsScrolling) { 333 if (mScroller.isScrollOutOfBounds()) { 334 mScroller.animateBoundScroll(); 335 } else if (Math.abs(velocity) > mMinimumVelocity) { 336 float minY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, 337 layoutAlgorithm.mMaxScrollP); 338 float maxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, 339 layoutAlgorithm.mMinScrollP); 340 mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY, 341 mOverscrollSize); 342 mSv.invalidate(); 343 } 344 345 // Reset the focused task after the user has scrolled 346 if (!mSv.mTouchExplorationEnabled) { 347 mSv.resetFocusedTask(mSv.getFocusedTask()); 348 } 349 } else if (mActiveTaskView == null) { 350 // This tap didn't start on a task. 351 maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); 352 } 353 354 mActivePointerId = INACTIVE_POINTER_ID; 355 mIsScrolling = false; 356 recycleVelocityTracker(); 357 break; 358 } 359 case MotionEvent.ACTION_CANCEL: { 360 mActivePointerId = INACTIVE_POINTER_ID; 361 mIsScrolling = false; 362 recycleVelocityTracker(); 363 break; 364 } 365 } 366 return mIsScrolling; 367 } 368 369 /** Hides recents if the up event at (x, y) is a tap on the background area. */ maybeHideRecentsFromBackgroundTap(int x, int y)370 void maybeHideRecentsFromBackgroundTap(int x, int y) { 371 // Ignore the up event if it's too far from its start position. The user might have been 372 // trying to scroll or swipe. 373 int dx = Math.abs(mDownX - x); 374 int dy = Math.abs(mDownY - y); 375 if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { 376 return; 377 } 378 379 // Shift the tap position toward the center of the task stack and check to see if it would 380 // have hit a view. The user might have tried to tap on a task and missed slightly. 381 int shiftedX = x; 382 if (x > (mSv.getRight() - mSv.getLeft()) / 2) { 383 shiftedX -= mWindowTouchSlop; 384 } else { 385 shiftedX += mWindowTouchSlop; 386 } 387 if (findViewAtPoint(shiftedX, y) != null) { 388 return; 389 } 390 391 // Disallow tapping above and below the stack to dismiss recents 392 if (x > mSv.mLayoutAlgorithm.mStackRect.left && x < mSv.mLayoutAlgorithm.mStackRect.right) { 393 return; 394 } 395 396 // If tapping on the freeform workspace background, just launch the first freeform task 397 SystemServicesProxy ssp = Recents.getSystemServices(); 398 if (ssp.hasFreeformWorkspaceSupport()) { 399 Rect freeformRect = mSv.mLayoutAlgorithm.mFreeformRect; 400 if (freeformRect.top <= y && y <= freeformRect.bottom) { 401 if (mSv.launchFreeformTasks()) { 402 // TODO: Animate Recents away as we launch the freeform tasks 403 return; 404 } 405 } 406 } 407 408 // The user intentionally tapped on the background, which is like a tap on the "desktop". 409 // Hide recents and transition to the launcher. 410 EventBus.getDefault().send(new HideRecentsEvent(false, true)); 411 } 412 413 /** Handles generic motion events */ onGenericMotionEvent(MotionEvent ev)414 public boolean onGenericMotionEvent(MotionEvent ev) { 415 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 416 InputDevice.SOURCE_CLASS_POINTER) { 417 int action = ev.getAction(); 418 switch (action & MotionEvent.ACTION_MASK) { 419 case MotionEvent.ACTION_SCROLL: 420 // Find the front most task and scroll the next task to the front 421 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 422 if (vScroll > 0) { 423 mSv.setRelativeFocusedTask(true, true /* stackTasksOnly */, 424 false /* animated */); 425 } else { 426 mSv.setRelativeFocusedTask(false, true /* stackTasksOnly */, 427 false /* animated */); 428 } 429 return true; 430 } 431 } 432 return false; 433 } 434 435 /**** SwipeHelper Implementation ****/ 436 437 @Override getChildAtPosition(MotionEvent ev)438 public View getChildAtPosition(MotionEvent ev) { 439 TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY()); 440 if (tv != null && canChildBeDismissed(tv)) { 441 return tv; 442 } 443 return null; 444 } 445 446 @Override canChildBeDismissed(View v)447 public boolean canChildBeDismissed(View v) { 448 // Disallow dismissing an already dismissed task 449 TaskView tv = (TaskView) v; 450 Task task = tv.getTask(); 451 return !mSwipeHelperAnimations.containsKey(v) && 452 (mSv.getStack().indexOfStackTask(task) != -1); 453 } 454 455 /** 456 * Starts a manual drag that goes through the same swipe helper path. 457 */ onBeginManualDrag(TaskView v)458 public void onBeginManualDrag(TaskView v) { 459 mActiveTaskView = v; 460 mSwipeHelperAnimations.put(v, null); 461 onBeginDrag(v); 462 } 463 464 @Override onBeginDrag(View v)465 public void onBeginDrag(View v) { 466 TaskView tv = (TaskView) v; 467 468 // Disable clipping with the stack while we are swiping 469 tv.setClipViewInStack(false); 470 // Disallow touch events from this task view 471 tv.setTouchEnabled(false); 472 // Disallow parents from intercepting touch events 473 final ViewParent parent = mSv.getParent(); 474 if (parent != null) { 475 parent.requestDisallowInterceptTouchEvent(true); 476 } 477 478 // Add this task to the set of tasks we are deleting 479 mSv.addIgnoreTask(tv.getTask()); 480 481 // Determine if we are animating the other tasks while dismissing this task 482 mCurrentTasks = new ArrayList<Task>(mSv.getStack().getStackTasks()); 483 MutableBoolean isFrontMostTask = new MutableBoolean(false); 484 Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask); 485 TaskStackLayoutAlgorithm layoutAlgorithm = mSv.getStackAlgorithm(); 486 TaskStackViewScroller stackScroller = mSv.getScroller(); 487 if (anchorTask != null) { 488 // Get the current set of task transforms 489 mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms); 490 491 // Get the stack scroll of the task to anchor to (since we are removing something, the 492 // front most task will be our anchor task) 493 float prevAnchorTaskScroll = 0; 494 boolean pullStackForward = mCurrentTasks.size() > 0; 495 if (pullStackForward) { 496 prevAnchorTaskScroll = layoutAlgorithm.getStackScrollForTask(anchorTask); 497 } 498 499 // Calculate where the views would be without the deleting tasks 500 mSv.updateLayoutAlgorithm(false /* boundScroll */); 501 502 float newStackScroll = stackScroller.getStackScroll(); 503 if (isFrontMostTask.value) { 504 // Bound the stack scroll to pull tasks forward if necessary 505 newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll); 506 } else if (pullStackForward) { 507 // Otherwise, offset the scroll by the movement of the anchor task 508 float anchorTaskScroll = 509 layoutAlgorithm.getStackScrollForTaskIgnoreOverrides(anchorTask); 510 float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll); 511 if (layoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) { 512 // If we are focused, we don't want the front task to move, but otherwise, we 513 // allow the back task to move up, and the front task to move back 514 stackScrollOffset *= 0.75f; 515 } 516 newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll() 517 + stackScrollOffset); 518 } 519 520 // Pick up the newly visible views, not including the deleting tasks 521 mSv.bindVisibleTaskViews(newStackScroll, true /* ignoreTaskOverrides */); 522 523 // Get the final set of task transforms (with task removed) 524 mSv.getLayoutTaskTransforms(newStackScroll, TaskStackLayoutAlgorithm.STATE_UNFOCUSED, 525 mCurrentTasks, true /* ignoreTaskOverrides */, mFinalTaskTransforms); 526 527 // Set the target to scroll towards upon dismissal 528 mTargetStackScroll = newStackScroll; 529 530 /* 531 * Post condition: All views that will be visible as a part of the gesture are retrieved 532 * and at their initial positions. The stack is still at the current 533 * scroll, but the layout is updated without the task currently being 534 * dismissed. The final layout is in the unfocused stack state, which 535 * will be applied when the current task is dismissed. 536 */ 537 } 538 } 539 540 @Override updateSwipeProgress(View v, boolean dismissable, float swipeProgress)541 public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) { 542 // Only update the swipe progress for the surrounding tasks if the dismiss animation was not 543 // preempted from a call to cancelNonDismissTaskAnimations 544 if (mActiveTaskView == v || mSwipeHelperAnimations.containsKey(v)) { 545 updateTaskViewTransforms( 546 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(swipeProgress)); 547 } 548 return true; 549 } 550 551 /** 552 * Called after the {@link TaskView} is finished animating away. 553 */ 554 @Override onChildDismissed(View v)555 public void onChildDismissed(View v) { 556 TaskView tv = (TaskView) v; 557 558 // Re-enable clipping with the stack (we will reuse this view) 559 tv.setClipViewInStack(true); 560 // Re-enable touch events from this task view 561 tv.setTouchEnabled(true); 562 // Remove the task view from the stack, ignoring the animation if we've started dragging 563 // again 564 EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv, 565 mSwipeHelperAnimations.containsKey(v) 566 ? new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION, 567 Interpolators.FAST_OUT_SLOW_IN) 568 : null)); 569 // Only update the final scroll and layout state (set in onBeginDrag()) if the dismiss 570 // animation was not preempted from a call to cancelNonDismissTaskAnimations 571 if (mSwipeHelperAnimations.containsKey(v)) { 572 // Update the scroll to the final scroll position 573 mSv.getScroller().setStackScroll(mTargetStackScroll, null); 574 // Update the focus state to the final focus state 575 mSv.getStackAlgorithm().setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); 576 mSv.getStackAlgorithm().clearUnfocusedTaskOverrides(); 577 // Stop tracking this deletion animation 578 mSwipeHelperAnimations.remove(v); 579 } 580 // Keep track of deletions by keyboard 581 MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", 582 Constants.Metrics.DismissSourceSwipeGesture); 583 } 584 585 /** 586 * Called after the {@link TaskView} is finished animating back into the list. 587 * onChildDismissed() calls. 588 */ 589 @Override onChildSnappedBack(View v, float targetLeft)590 public void onChildSnappedBack(View v, float targetLeft) { 591 TaskView tv = (TaskView) v; 592 593 // Re-enable clipping with the stack 594 tv.setClipViewInStack(true); 595 // Re-enable touch events from this task view 596 tv.setTouchEnabled(true); 597 598 // Stop tracking this deleting task, and update the layout to include this task again. The 599 // stack scroll does not need to be reset, since the scroll has not actually changed in 600 // onBeginDrag(). 601 mSv.removeIgnoreTask(tv.getTask()); 602 mSv.updateLayoutAlgorithm(false /* boundScroll */); 603 mSv.relayoutTaskViews(AnimationProps.IMMEDIATE); 604 mSwipeHelperAnimations.remove(v); 605 } 606 607 @Override onDragCancelled(View v)608 public void onDragCancelled(View v) { 609 // Do nothing 610 } 611 612 @Override isAntiFalsingNeeded()613 public boolean isAntiFalsingNeeded() { 614 return false; 615 } 616 617 @Override getFalsingThresholdFactor()618 public float getFalsingThresholdFactor() { 619 return 0; 620 } 621 622 /** 623 * Interpolates the non-deleting tasks to their final transforms from their current transforms. 624 */ updateTaskViewTransforms(float dismissFraction)625 private void updateTaskViewTransforms(float dismissFraction) { 626 List<TaskView> taskViews = mSv.getTaskViews(); 627 int taskViewCount = taskViews.size(); 628 for (int i = 0; i < taskViewCount; i++) { 629 TaskView tv = taskViews.get(i); 630 Task task = tv.getTask(); 631 632 if (mSv.isIgnoredTask(task)) { 633 continue; 634 } 635 636 int taskIndex = mCurrentTasks.indexOf(task); 637 TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex); 638 TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex); 639 640 mTmpTransform.copyFrom(fromTransform); 641 // We only really need to interpolate the bounds, progress and translation 642 mTmpTransform.rect.set(Utilities.RECTF_EVALUATOR.evaluate(dismissFraction, 643 fromTransform.rect, toTransform.rect)); 644 mTmpTransform.dimAlpha = fromTransform.dimAlpha + (toTransform.dimAlpha - 645 fromTransform.dimAlpha) * dismissFraction; 646 mTmpTransform.viewOutlineAlpha = fromTransform.viewOutlineAlpha + 647 (toTransform.viewOutlineAlpha - fromTransform.viewOutlineAlpha) * 648 dismissFraction; 649 mTmpTransform.translationZ = fromTransform.translationZ + 650 (toTransform.translationZ - fromTransform.translationZ) * dismissFraction; 651 652 mSv.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE); 653 } 654 } 655 656 /** Returns the view at the specified coordinates */ findViewAtPoint(int x, int y)657 private TaskView findViewAtPoint(int x, int y) { 658 List<Task> tasks = mSv.getStack().getStackTasks(); 659 int taskCount = tasks.size(); 660 for (int i = taskCount - 1; i >= 0; i--) { 661 TaskView tv = mSv.getChildViewForTask(tasks.get(i)); 662 if (tv != null && tv.getVisibility() == View.VISIBLE) { 663 if (mSv.isTouchPointInView(x, y, tv)) { 664 return tv; 665 } 666 } 667 } 668 return null; 669 } 670 671 /** 672 * Returns the scaled size used to calculate the dismiss fraction. 673 */ getScaledDismissSize()674 public float getScaledDismissSize() { 675 return 1.5f * Math.max(mSv.getWidth(), mSv.getHeight()); 676 } 677 } 678