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