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