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