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