Animate the visible task view if launching an app that resolves to the task

Test: Manual, launch app for associated visible task
Change-Id: I7a56553197ad23e1269eb50523eca0ea88898f47
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 11bc883..2f0cd78 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -47,6 +47,10 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.DrawableFactory;
+import com.android.quickstep.RecentsAnimationInterpolator;
+import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TaskView;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -70,6 +74,7 @@
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
             "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
 
+    private static final int RECENTS_LAUNCH_DURATION = 336;
     private static final int LAUNCHER_RESUME_START_DELAY = 150;
     private static final int CLOSING_TRANSITION_DURATION_MS = 350;
 
@@ -139,8 +144,18 @@
                         // Post at front of queue ignoring sync barriers to make sure it gets
                         // processed before the next frame.
                         postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
-                            LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
-                                    getLauncherAnimators(v), getWindowAnimators(v, targets));
+                            final boolean removeTrackingView;
+                            LauncherTransitionAnimator animator =
+                                    composeRecentsLaunchAnimator(v, targets);
+                            if (animator != null) {
+                                // We are animating the task view directly, do not remove it after
+                                removeTrackingView = false;
+                            } else {
+                                animator = composeAppLaunchAnimator(v, targets);
+                                // A new floating view is created for the animation, remove it after
+                                removeTrackingView = true;
+                            }
+
                             setCurrentAnimator(animator);
                             mAnimator = animator.getAnimatorSet();
                             mAnimator.addListener(new AnimatorListenerAdapter() {
@@ -148,7 +163,10 @@
                                 public void onAnimationEnd(Animator animation) {
                                     // Reset launcher to normal state
                                     v.setVisibility(View.VISIBLE);
-                                    ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+                                    if (removeTrackingView) {
+                                        ((ViewGroup) mDragLayer.getParent()).removeView(
+                                                mFloatingView);
+                                    }
 
                                     mDragLayer.setAlpha(1f);
                                     mDragLayer.setTranslationY(0f);
@@ -179,6 +197,131 @@
     }
 
     /**
+     * Composes the animations for a launch from the recents list if possible.
+     */
+    private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
+        // Ensure recents is actually visible
+        if (!mLauncher.isInState(LauncherState.OVERVIEW)) {
+            return null;
+        }
+
+        // Resolve the opening task id
+        int openingTaskId = -1;
+        for (RemoteAnimationTargetCompat target : targets) {
+            if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                openingTaskId = target.taskId;
+                break;
+            }
+        }
+
+        // If there is no opening task id, fall back to the normal app icon launch animation
+        if (openingTaskId == -1) {
+            return null;
+        }
+
+        // If the opening task id is not currently visible in overview, then fall back to normal app
+        // icon launch animation
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        TaskView taskView = recentsView.getTaskView(openingTaskId);
+        if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
+            return null;
+        }
+
+        // Found a visible recents task that matches the opening app, lets launch the app from there
+        return new LauncherTransitionAnimator(null, getRecentsWindowAnimator(taskView, targets));
+    }
+
+    /**
+     * @return Animator that controls the window of the opening targets for the recents launch
+     * animation.
+     */
+    private ValueAnimator getRecentsWindowAnimator(TaskView v,
+            RemoteAnimationTargetCompat[] targets) {
+        Rect taskViewBounds = new Rect();
+        mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
+
+        // TODO: Use the actual target insets instead of the current thumbnail insets in case the
+        // device state has changed
+        RecentsAnimationInterpolator recentsInterpolator = new RecentsAnimationInterpolator(
+                new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx),
+                v.getThumbnail().getInsets(),
+                taskViewBounds, new Rect(0, v.getThumbnail().getTop(), 0, 0));
+
+        Rect crop = new Rect();
+        Matrix matrix = new Matrix();
+
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+        appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            boolean isFirstFrame = true;
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final Surface surface = getSurface(v);
+                final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
+                if (frameNumber == -1) {
+                    // Booo, not cool! Our surface got destroyed, so no reason to animate anything.
+                    Log.w(TAG, "Failed to animate, surface got destroyed.");
+                    return;
+                }
+                final float percent = animation.getAnimatedFraction();
+                TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
+
+                v.setScaleX(tw.taskScale);
+                v.setScaleY(tw.taskScale);
+                v.setTranslationX(tw.taskX);
+                v.setTranslationY(tw.taskY);
+                // Defer fading out the view until after the app window gets faded in
+                v.setAlpha(getValue(1f, 0f, 75, 75,
+                        appAnimator.getDuration() * percent, Interpolators.LINEAR));
+
+                matrix.setScale(tw.winScale, tw.winScale);
+                matrix.postTranslate(tw.winX, tw.winY);
+                crop.set(tw.winCrop);
+
+                // Fade in the app window.
+                float alphaDelay = 0;
+                float alphaDuration = 75;
+                float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
+                        appAnimator.getDuration() * percent, Interpolators.LINEAR);
+
+                TransactionCompat t = new TransactionCompat();
+                for (RemoteAnimationTargetCompat target : targets) {
+                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                        t.setAlpha(target.leash, alpha);
+
+                        // TODO: This isn't correct at the beginning of the animation, but better
+                        // than nothing.
+                        matrix.postTranslate(target.position.x, target.position.y);
+                        t.setMatrix(target.leash, matrix);
+                        t.setWindowCrop(target.leash, crop);
+                        t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
+                    }
+                    if (isFirstFrame) {
+                        t.show(target.leash);
+                    }
+                }
+                t.apply();
+
+                matrix.reset();
+                isFirstFrame = false;
+            }
+        });
+        return appAnimator;
+    }
+
+    /**
+     * Composes the animations for a launch from an app icon.
+     */
+    private LauncherTransitionAnimator composeAppLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
+        return new LauncherTransitionAnimator(getLauncherAnimators(v),
+                getWindowAnimators(v, targets));
+    }
+
+    /**
      * @return Animators that control the movements of the Launcher and icon of the opening target.
      */
     private AnimatorSet getLauncherAnimators(View v) {
diff --git a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
index 80eaef7..aec2869 100644
--- a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
+++ b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
@@ -32,11 +32,15 @@
     private Animator mWindowAnimator;
 
     LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) {
-        mLauncherAnimator = launcherAnimator;
+        if (launcherAnimator != null) {
+            mLauncherAnimator = launcherAnimator;
+        }
         mWindowAnimator = windowAnimator;
 
         mAnimatorSet = new AnimatorSet();
-        mAnimatorSet.play(launcherAnimator);
+        if (launcherAnimator != null) {
+            mAnimatorSet.play(launcherAnimator);
+        }
         mAnimatorSet.play(windowAnimator);
     }
 
@@ -53,6 +57,8 @@
     }
 
     public void finishLauncherAnimation() {
-        mLauncherAnimator.end();
+        if (mLauncherAnimator != null) {
+            mLauncherAnimator.end();
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
new file mode 100644
index 0000000..9cc038f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.graphics.Rect;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Helper class to interpolate the animation between a task view representation and an actual
+ * window.
+ */
+public class RecentsAnimationInterpolator {
+
+    public static class TaskWindowBounds {
+        public float taskScale = 1f;
+        public float taskX = 0f;
+        public float taskY = 0f;
+
+        public float winScale = 1f;
+        public float winX = 0f;
+        public float winY = 0f;
+        public Rect winCrop = new Rect();
+
+        @Override
+        public String toString() {
+            return "taskScale=" + taskScale + " taskX=" + taskX + " taskY=" + taskY
+                    + " winScale=" + winScale + " winX=" + winX + " winY=" + winY
+                    + " winCrop=" + winCrop;
+        }
+    }
+
+    private TaskWindowBounds mTmpTaskWindowBounds = new TaskWindowBounds();
+    private Rect mTmpInsets = new Rect();
+
+    private Rect mWindow;
+    private Rect mInsetWindow;
+    private Rect mInsets;
+    private Rect mTask;
+    private Rect mTaskInsets;
+    private Rect mThumbnail;
+
+    private float mTaskScale;
+    private Rect mScaledTask;
+    private Rect mTargetTask;
+    private Rect mSrcWindow;
+
+    public RecentsAnimationInterpolator(Rect window, Rect insets, Rect task, Rect taskInsets) {
+        mWindow = window;
+        mInsets = insets;
+        mTask = task;
+        mTaskInsets = taskInsets;
+        mInsetWindow = new Rect(window);
+        Utilities.insetRect(mInsetWindow, insets);
+
+        mThumbnail = new Rect(task);
+        Utilities.insetRect(mThumbnail, taskInsets);
+        mTaskScale = (float) mInsetWindow.width() / mThumbnail.width();
+        mScaledTask = new Rect(task);
+        Utilities.scaleRectAboutCenter(mScaledTask, mTaskScale);
+        Rect finalScaledTaskInsets = new Rect(taskInsets);
+        Utilities.scaleRect(finalScaledTaskInsets, mTaskScale);
+        mTargetTask = new Rect(mInsetWindow);
+        mTargetTask.offsetTo(window.top + insets.top - finalScaledTaskInsets.top,
+                window.left + insets.left - finalScaledTaskInsets.left);
+
+        float initialWinScale = 1f / mTaskScale;
+        Rect scaledWindow = new Rect(mInsetWindow);
+        Utilities.scaleRectAboutCenter(scaledWindow, initialWinScale);
+        Rect scaledInsets = new Rect(insets);
+        Utilities.scaleRect(scaledInsets, initialWinScale);
+        mSrcWindow = new Rect(scaledWindow);
+        mSrcWindow.offsetTo(mThumbnail.left - scaledInsets.left,
+                mThumbnail.top - scaledInsets.top);
+    }
+
+    public TaskWindowBounds interpolate(float t) {
+        mTmpTaskWindowBounds.taskScale = Utilities.mapRange(t,
+                1, (float) mInsetWindow.width() / mThumbnail.width());
+        mTmpTaskWindowBounds.taskX = Utilities.mapRange(t,
+                0, mTargetTask.left - mScaledTask.left);
+        mTmpTaskWindowBounds.taskY = Utilities.mapRange(t,
+                0, mTargetTask.top - mScaledTask.top);
+
+        mTmpTaskWindowBounds.winScale = mTmpTaskWindowBounds.taskScale / mTaskScale;
+        mTmpTaskWindowBounds.winX = Utilities.mapRange(t,
+                mSrcWindow.left, 0);
+        mTmpTaskWindowBounds.winY = Utilities.mapRange(t,
+                mSrcWindow.top, 0);
+
+        mTmpInsets.set(mInsets);
+        Utilities.scaleRect(mTmpInsets, (1f - t));
+        mTmpTaskWindowBounds.winCrop.set(mWindow);
+        Utilities.insetRect(mTmpTaskWindowBounds.winCrop, mTmpInsets);
+
+        return mTmpTaskWindowBounds;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 8e03f37..26fe54e 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -215,6 +215,21 @@
         return mFirstTaskIndex;
     }
 
+    public boolean isTaskViewVisible(TaskView tv) {
+        // For now, just check if it's the active task
+        return indexOfChild(tv) == getNextPage();
+    }
+
+    public TaskView getTaskView(int taskId) {
+        for (int i = getFirstTaskIndex(); i < getChildCount(); i++) {
+            TaskView tv = (TaskView) getChildAt(i);
+            if (tv.getTask().key.id == taskId) {
+                return tv;
+            }
+        }
+        return null;
+    }
+
     public void setStateController(RecentsViewStateController stateController) {
         mStateController = stateController;
     }
@@ -254,11 +269,16 @@
         }
         setLayoutTransition(mLayoutTransition);
 
-        // Rebind all task views
+        // Rebind and reset all task views
         for (int i = tasks.size() - 1; i >= 0; i--) {
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1 + mFirstTaskIndex);
             taskView.bind(task);
+            taskView.setScaleX(1f);
+            taskView.setScaleY(1f);
+            taskView.setTranslationX(0f);
+            taskView.setTranslationY(0f);
+            taskView.setAlpha(1f);
             loader.loadTaskData(task);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 36a0601..4f93b1c 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -28,6 +28,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.view.View;
@@ -108,6 +109,13 @@
         updateThumbnailPaintFilter();
     }
 
+    public Rect getInsets() {
+        if (mThumbnailData != null) {
+            return mThumbnailData.insets;
+        }
+        return new Rect();
+    }
+
     @Override
     protected void onDraw(Canvas canvas) {
         canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 158c540..d559b44 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -237,16 +237,27 @@
             int cx = r.centerX();
             int cy = r.centerY();
             r.offset(-cx, -cy);
+            scaleRect(r, scale);
+            r.offset(cx, cy);
+        }
+    }
 
+    public static void scaleRect(Rect r, float scale) {
+        if (scale != 1.0f) {
             r.left = (int) (r.left * scale + 0.5f);
             r.top = (int) (r.top * scale + 0.5f);
             r.right = (int) (r.right * scale + 0.5f);
             r.bottom = (int) (r.bottom * scale + 0.5f);
-
-            r.offset(cx, cy);
         }
     }
 
+    public static void insetRect(Rect r, Rect insets) {
+        r.left = Math.min(r.right, r.left + insets.left);
+        r.top = Math.min(r.bottom, r.top + insets.top);
+        r.right = Math.max(r.left, r.right - insets.right);
+        r.bottom = Math.max(r.top, r.bottom - insets.bottom);
+    }
+
     public static float shrinkRect(Rect r, float scaleX, float scaleY) {
         float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
         if (scale < 1.0f) {
@@ -261,6 +272,10 @@
         return scale;
     }
 
+    public static float mapRange(float value, float min, float max) {
+        return min + (value * (max - min));
+    }
+
     public static boolean isSystemApp(Context context, Intent intent) {
         PackageManager pm = context.getPackageManager();
         ComponentName cn = intent.getComponent();
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 2343654..0dcebe3 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -50,6 +50,9 @@
 
     public static final Interpolator OVERSHOOT_0 = new OvershootInterpolator(0);
 
+    public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
     /**
      * Inversion of zInterpolate, compounded with an ease-out.
      */