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.nano.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 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 */ 154 void initOrResetVelocityTracker() { 155 if (mVelocityTracker == null) { 156 mVelocityTracker = VelocityTracker.obtain(); 157 } else { 158 mVelocityTracker.clear(); 159 } 160 } 161 void recycleVelocityTracker() { 162 if (mVelocityTracker != null) { 163 mVelocityTracker.recycle(); 164 mVelocityTracker = null; 165 } 166 } 167 168 /** Touch preprocessing for handling below */ 169 public boolean onInterceptTouchEvent(MotionEvent ev) { 170 // Pass through to swipe helper if we are swiping 171 mInterceptedBySwipeHelper = isSwipingEnabled() && 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 */ 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 */ 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 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 mLastY = mDownY = y; 283 } 284 } 285 if (mIsScrolling) { 286 // If we just move linearly on the screen, then that would map to 1/arclength 287 // of the curve, so just move the scroll proportional to that 288 float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y); 289 290 // Modulate the overscroll to prevent users from pulling the stack too far 291 float minScrollP = layoutAlgorithm.mMinScrollP; 292 float maxScrollP = layoutAlgorithm.mMaxScrollP; 293 float curScrollP = mDownScrollP + deltaP; 294 if (curScrollP < minScrollP || curScrollP > maxScrollP) { 295 float clampedScrollP = Utilities.clamp(curScrollP, minScrollP, maxScrollP); 296 float overscrollP = (curScrollP - clampedScrollP); 297 float overscrollX = Math.abs(overscrollP) / MAX_OVERSCROLL; 298 float interpX = OVERSCROLL_INTERP.getInterpolation(overscrollX); 299 curScrollP = clampedScrollP + Math.signum(overscrollP) * 300 (interpX * MAX_OVERSCROLL); 301 } 302 mDownScrollP += mScroller.setDeltaStackScroll(mDownScrollP, 303 curScrollP - mDownScrollP); 304 mStackViewScrolledEvent.updateY(y - mLastY); 305 EventBus.getDefault().send(mStackViewScrolledEvent); 306 } 307 308 mLastY = y; 309 mVelocityTracker.addMovement(ev); 310 break; 311 } 312 case MotionEvent.ACTION_POINTER_UP: { 313 int pointerIndex = ev.getActionIndex(); 314 int pointerId = ev.getPointerId(pointerIndex); 315 if (pointerId == mActivePointerId) { 316 // Select a new active pointer id and reset the motion state 317 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 318 mActivePointerId = ev.getPointerId(newPointerIndex); 319 mDownX = (int) ev.getX(pointerIndex); 320 mDownY = (int) ev.getY(pointerIndex); 321 mLastY = mDownY; 322 mDownScrollP = mScroller.getStackScroll(); 323 } 324 mVelocityTracker.addMovement(ev); 325 break; 326 } 327 case MotionEvent.ACTION_UP: { 328 mVelocityTracker.addMovement(ev); 329 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 330 int activePointerIndex = ev.findPointerIndex(mActivePointerId); 331 int y = (int) ev.getY(activePointerIndex); 332 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); 333 if (mIsScrolling) { 334 if (mScroller.isScrollOutOfBounds()) { 335 mScroller.animateBoundScroll(); 336 } else if (Math.abs(velocity) > mMinimumVelocity) { 337 float minY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, 338 layoutAlgorithm.mMaxScrollP); 339 float maxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, 340 layoutAlgorithm.mMinScrollP); 341 mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY, 342 mOverscrollSize); 343 mSv.invalidate(); 344 } 345 346 // Reset the focused task after the user has scrolled, but we have no scrolling 347 // in grid layout and therefore we don't want to reset the focus there. 348 if (!mSv.mTouchExplorationEnabled && !mSv.useGridLayout()) { 349 mSv.resetFocusedTask(mSv.getFocusedTask()); 350 } 351 } else if (mActiveTaskView == null) { 352 // This tap didn't start on a task. 353 maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); 354 } 355 356 mActivePointerId = INACTIVE_POINTER_ID; 357 mIsScrolling = false; 358 recycleVelocityTracker(); 359 break; 360 } 361 case MotionEvent.ACTION_CANCEL: { 362 mActivePointerId = INACTIVE_POINTER_ID; 363 mIsScrolling = false; 364 recycleVelocityTracker(); 365 break; 366 } 367 } 368 return mIsScrolling; 369 } 370 371 /** Hides recents if the up event at (x, y) is a tap on the background area. */ 372 void maybeHideRecentsFromBackgroundTap(int x, int y) { 373 // Ignore the up event if it's too far from its start position. The user might have been 374 // trying to scroll or swipe. 375 int dx = Math.abs(mDownX - x); 376 int dy = Math.abs(mDownY - y); 377 if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { 378 return; 379 } 380 381 // Shift the tap position toward the center of the task stack and check to see if it would 382 // have hit a view. The user might have tried to tap on a task and missed slightly. 383 int shiftedX = x; 384 if (x > (mSv.getRight() - mSv.getLeft()) / 2) { 385 shiftedX -= mWindowTouchSlop; 386 } else { 387 shiftedX += mWindowTouchSlop; 388 } 389 if (findViewAtPoint(shiftedX, y) != null) { 390 return; 391 } 392 393 // Disallow tapping above and below the stack to dismiss recents 394 if (x > mSv.mLayoutAlgorithm.mStackRect.left && x < mSv.mLayoutAlgorithm.mStackRect.right) { 395 return; 396 } 397 398 // If tapping on the freeform workspace background, just launch the first freeform task 399 SystemServicesProxy ssp = Recents.getSystemServices(); 400 if (ssp.hasFreeformWorkspaceSupport()) { 401 Rect freeformRect = mSv.mLayoutAlgorithm.mFreeformRect; 402 if (freeformRect.top <= y && y <= freeformRect.bottom) { 403 if (mSv.launchFreeformTasks()) { 404 // TODO: Animate Recents away as we launch the freeform tasks 405 return; 406 } 407 } 408 } 409 410 // The user intentionally tapped on the background, which is like a tap on the "desktop". 411 // Hide recents and transition to the launcher. 412 EventBus.getDefault().send(new HideRecentsEvent(false, true)); 413 } 414 415 /** Handles generic motion events */ 416 public boolean onGenericMotionEvent(MotionEvent ev) { 417 if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 418 InputDevice.SOURCE_CLASS_POINTER) { 419 int action = ev.getAction(); 420 switch (action & MotionEvent.ACTION_MASK) { 421 case MotionEvent.ACTION_SCROLL: 422 // Find the front most task and scroll the next task to the front 423 float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); 424 if (vScroll > 0) { 425 mSv.setRelativeFocusedTask(true, true /* stackTasksOnly */, 426 false /* animated */); 427 } else { 428 mSv.setRelativeFocusedTask(false, true /* stackTasksOnly */, 429 false /* animated */); 430 } 431 return true; 432 } 433 } 434 return false; 435 } 436 437 /**** SwipeHelper Implementation ****/ 438 439 @Override 440 public View getChildAtPosition(MotionEvent ev) { 441 TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY()); 442 if (tv != null && canChildBeDismissed(tv)) { 443 return tv; 444 } 445 return null; 446 } 447 448 @Override 449 public boolean canChildBeDismissed(View v) { 450 // Disallow dismissing an already dismissed task 451 TaskView tv = (TaskView) v; 452 Task task = tv.getTask(); 453 return !mSwipeHelperAnimations.containsKey(v) && 454 (mSv.getStack().indexOfStackTask(task) != -1); 455 } 456 457 /** 458 * Starts a manual drag that goes through the same swipe helper path. 459 */ 460 public void onBeginManualDrag(TaskView v) { 461 mActiveTaskView = v; 462 mSwipeHelperAnimations.put(v, null); 463 onBeginDrag(v); 464 } 465 466 @Override 467 public void onBeginDrag(View v) { 468 TaskView tv = (TaskView) v; 469 470 // Disable clipping with the stack while we are swiping 471 tv.setClipViewInStack(false); 472 // Disallow touch events from this task view 473 tv.setTouchEnabled(false); 474 // Disallow parents from intercepting touch events 475 final ViewParent parent = mSv.getParent(); 476 if (parent != null) { 477 parent.requestDisallowInterceptTouchEvent(true); 478 } 479 480 // Add this task to the set of tasks we are deleting 481 mSv.addIgnoreTask(tv.getTask()); 482 483 // Determine if we are animating the other tasks while dismissing this task 484 mCurrentTasks = new ArrayList<Task>(mSv.getStack().getStackTasks()); 485 MutableBoolean isFrontMostTask = new MutableBoolean(false); 486 Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask); 487 TaskStackLayoutAlgorithm layoutAlgorithm = mSv.getStackAlgorithm(); 488 TaskStackViewScroller stackScroller = mSv.getScroller(); 489 if (anchorTask != null) { 490 // Get the current set of task transforms 491 mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms); 492 493 // Get the stack scroll of the task to anchor to (since we are removing something, the 494 // front most task will be our anchor task) 495 float prevAnchorTaskScroll = 0; 496 boolean pullStackForward = mCurrentTasks.size() > 0; 497 if (pullStackForward) { 498 prevAnchorTaskScroll = layoutAlgorithm.getStackScrollForTask(anchorTask); 499 } 500 501 // Calculate where the views would be without the deleting tasks 502 mSv.updateLayoutAlgorithm(false /* boundScroll */); 503 504 float newStackScroll = stackScroller.getStackScroll(); 505 if (isFrontMostTask.value) { 506 // Bound the stack scroll to pull tasks forward if necessary 507 newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll); 508 } else if (pullStackForward) { 509 // Otherwise, offset the scroll by the movement of the anchor task 510 float anchorTaskScroll = 511 layoutAlgorithm.getStackScrollForTaskIgnoreOverrides(anchorTask); 512 float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll); 513 if (layoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) { 514 // If we are focused, we don't want the front task to move, but otherwise, we 515 // allow the back task to move up, and the front task to move back 516 stackScrollOffset *= 0.75f; 517 } 518 newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll() 519 + stackScrollOffset); 520 } 521 522 // Pick up the newly visible views, not including the deleting tasks 523 mSv.bindVisibleTaskViews(newStackScroll, true /* ignoreTaskOverrides */); 524 525 // Get the final set of task transforms (with task removed) 526 mSv.getLayoutTaskTransforms(newStackScroll, TaskStackLayoutAlgorithm.STATE_UNFOCUSED, 527 mCurrentTasks, true /* ignoreTaskOverrides */, mFinalTaskTransforms); 528 529 // Set the target to scroll towards upon dismissal 530 mTargetStackScroll = newStackScroll; 531 532 /* 533 * Post condition: All views that will be visible as a part of the gesture are retrieved 534 * and at their initial positions. The stack is still at the current 535 * scroll, but the layout is updated without the task currently being 536 * dismissed. The final layout is in the unfocused stack state, which 537 * will be applied when the current task is dismissed. 538 */ 539 } 540 } 541 542 @Override 543 public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) { 544 // Only update the swipe progress for the surrounding tasks if the dismiss animation was not 545 // preempted from a call to cancelNonDismissTaskAnimations 546 if (mActiveTaskView == v || mSwipeHelperAnimations.containsKey(v)) { 547 updateTaskViewTransforms( 548 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(swipeProgress)); 549 } 550 return true; 551 } 552 553 /** 554 * Called after the {@link TaskView} is finished animating away. 555 */ 556 @Override 557 public void onChildDismissed(View v) { 558 TaskView tv = (TaskView) v; 559 560 // Re-enable clipping with the stack (we will reuse this view) 561 tv.setClipViewInStack(true); 562 // Re-enable touch events from this task view 563 tv.setTouchEnabled(true); 564 // Remove the task view from the stack, ignoring the animation if we've started dragging 565 // again 566 EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv, 567 mSwipeHelperAnimations.containsKey(v) 568 ? new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION, 569 Interpolators.FAST_OUT_SLOW_IN) 570 : null)); 571 // Only update the final scroll and layout state (set in onBeginDrag()) if the dismiss 572 // animation was not preempted from a call to cancelNonDismissTaskAnimations 573 if (mSwipeHelperAnimations.containsKey(v)) { 574 // Update the scroll to the final scroll position 575 mSv.getScroller().setStackScroll(mTargetStackScroll, null); 576 // Update the focus state to the final focus state 577 mSv.getStackAlgorithm().setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); 578 mSv.getStackAlgorithm().clearUnfocusedTaskOverrides(); 579 // Stop tracking this deletion animation 580 mSwipeHelperAnimations.remove(v); 581 } 582 // Keep track of deletions by keyboard 583 MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", 584 Constants.Metrics.DismissSourceSwipeGesture); 585 } 586 587 /** 588 * Called after the {@link TaskView} is finished animating back into the list. 589 * onChildDismissed() calls. 590 */ 591 @Override 592 public void onChildSnappedBack(View v, float targetLeft) { 593 TaskView tv = (TaskView) v; 594 595 // Re-enable clipping with the stack 596 tv.setClipViewInStack(true); 597 // Re-enable touch events from this task view 598 tv.setTouchEnabled(true); 599 600 // Stop tracking this deleting task, and update the layout to include this task again. The 601 // stack scroll does not need to be reset, since the scroll has not actually changed in 602 // onBeginDrag(). 603 mSv.removeIgnoreTask(tv.getTask()); 604 mSv.updateLayoutAlgorithm(false /* boundScroll */); 605 mSv.relayoutTaskViews(AnimationProps.IMMEDIATE); 606 mSwipeHelperAnimations.remove(v); 607 } 608 609 @Override 610 public void onDragCancelled(View v) { 611 // Do nothing 612 } 613 614 @Override 615 public boolean isAntiFalsingNeeded() { 616 return false; 617 } 618 619 @Override 620 public float getFalsingThresholdFactor() { 621 return 0; 622 } 623 624 /** 625 * Interpolates the non-deleting tasks to their final transforms from their current transforms. 626 */ 627 private void updateTaskViewTransforms(float dismissFraction) { 628 List<TaskView> taskViews = mSv.getTaskViews(); 629 int taskViewCount = taskViews.size(); 630 for (int i = 0; i < taskViewCount; i++) { 631 TaskView tv = taskViews.get(i); 632 Task task = tv.getTask(); 633 634 if (mSv.isIgnoredTask(task)) { 635 continue; 636 } 637 638 int taskIndex = mCurrentTasks.indexOf(task); 639 if (taskIndex == -1) { 640 // If a task was added to the stack view after the start of the dismiss gesture, 641 // just ignore it 642 continue; 643 } 644 645 TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex); 646 TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex); 647 648 mTmpTransform.copyFrom(fromTransform); 649 // We only really need to interpolate the bounds, progress and translation 650 mTmpTransform.rect.set(Utilities.RECTF_EVALUATOR.evaluate(dismissFraction, 651 fromTransform.rect, toTransform.rect)); 652 mTmpTransform.dimAlpha = fromTransform.dimAlpha + (toTransform.dimAlpha - 653 fromTransform.dimAlpha) * dismissFraction; 654 mTmpTransform.viewOutlineAlpha = fromTransform.viewOutlineAlpha + 655 (toTransform.viewOutlineAlpha - fromTransform.viewOutlineAlpha) * 656 dismissFraction; 657 mTmpTransform.translationZ = fromTransform.translationZ + 658 (toTransform.translationZ - fromTransform.translationZ) * dismissFraction; 659 660 mSv.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE); 661 } 662 } 663 664 /** Returns the view at the specified coordinates */ 665 private TaskView findViewAtPoint(int x, int y) { 666 List<Task> tasks = mSv.getStack().getStackTasks(); 667 int taskCount = tasks.size(); 668 for (int i = taskCount - 1; i >= 0; i--) { 669 TaskView tv = mSv.getChildViewForTask(tasks.get(i)); 670 if (tv != null && tv.getVisibility() == View.VISIBLE) { 671 if (mSv.isTouchPointInView(x, y, tv)) { 672 return tv; 673 } 674 } 675 } 676 return null; 677 } 678 679 /** 680 * Returns the scaled size used to calculate the dismiss fraction. 681 */ 682 public float getScaledDismissSize() { 683 return 1.5f * Math.max(mSv.getWidth(), mSv.getHeight()); 684 } 685 686 /** 687 * Returns whether swiping is enabled. 688 */ 689 private boolean isSwipingEnabled() { 690 return !mSv.useGridLayout(); 691 } 692 } 693