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