Synchronized opening app transition animations.

Bug: 70220260
Change-Id: I3c8e1c477266fb3bd7a39f74e3e1191e82ce58e9
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 08c740c4..02b4379 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -23,6 +23,7 @@
 
     <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
 
+    <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
         android:fullBackupOnly="true"
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index b3025ff..18ddeee 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 222a3f4..a09e38d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -27,4 +27,7 @@
 
     <!-- TODO: This can be calculated using other resource values -->
     <dimen name="all_apps_search_box_full_height">90dp</dimen>
+
+    <dimen name="drag_layer_trans_y">25dp</dimen>
+
 </resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java
new file mode 100644
index 0000000..d2777f0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -0,0 +1,294 @@
+/*
+ * 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.launcher3;
+
+import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
+import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
+import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+
+import com.android.launcher3.InsettableFrameLayout.LayoutParams;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Manages the opening app animations from Launcher.
+ */
+public class LauncherAppTransitionManager {
+
+    private static final int REFRESH_RATE_MS = 16;
+
+    private final DragLayer mDragLayer;
+    private final Launcher mLauncher;
+    private final DeviceProfile mDeviceProfile;
+
+    private final float mDragLayerTransY;
+
+    private ImageView mFloatingView;
+
+    public LauncherAppTransitionManager(Launcher launcher) {
+        mLauncher = launcher;
+        mDragLayer = launcher.getDragLayer();
+        mDeviceProfile = launcher.getDeviceProfile();
+
+        mDragLayerTransY =
+                launcher.getResources().getDimensionPixelSize(R.dimen.drag_layer_trans_y);
+    }
+
+    public Bundle getActivityLauncherOptions(View v) {
+        RemoteAnimationRunnerCompat runner = new RemoteAnimationRunnerCompat() {
+            @Override
+            public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
+                    Runnable finishedCallback) {
+                // Post at front of queue ignoring sync barriers to make sure it gets processed
+                // before the next frame.
+                postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
+                    AnimatorSet both = new AnimatorSet();
+                    both.play(getLauncherAnimators(v));
+                    both.play(getAppWindowAnimators(v, targets));
+                    both.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            // Reset launcher to normal state
+                            v.setVisibility(View.VISIBLE);
+                            ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+
+                            mDragLayer.setAlpha(1f);
+                            mDragLayer.setTranslationY(0f);
+                            mDragLayer.getBackground().setAlpha(255);
+                            finishedCallback.run();
+                        }
+                    });
+                    both.start();
+                    // Because t=0 has the app icon in its original spot, we can skip the first
+                    // frame and have the same movement one frame earlier.
+                    both.setCurrentPlayTime(REFRESH_RATE_MS);
+                });
+            }
+
+            @Override
+            public void onAnimationCancelled() {
+            }
+        };
+
+        return ActivityOptionsCompat.makeRemoteAnimation(
+                new RemoteAnimationAdapterCompat(runner, 500, 380)).toBundle();
+    }
+
+    private AnimatorSet getLauncherAnimators(View v) {
+        AnimatorSet launcherAnimators = new AnimatorSet();
+        launcherAnimators.play(getHideLauncherAnimator());
+        launcherAnimators.play(getAppIconAnimator(v));
+        return launcherAnimators;
+    }
+
+    private AnimatorSet getHideLauncherAnimator() {
+        AnimatorSet hideLauncher = new AnimatorSet();
+
+        // Fade out the scrim fast to avoid the hard line
+        ObjectAnimator scrimAlpha = ObjectAnimator.ofInt(mDragLayer.getBackground(),
+                LauncherAnimUtils.DRAWABLE_ALPHA, 255, 0);
+        scrimAlpha.setDuration(130);
+        scrimAlpha.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+
+        // Animate Launcher so that it moves downwards and fades out.
+        ObjectAnimator dragLayerAlpha = ObjectAnimator.ofFloat(mDragLayer, View.ALPHA, 1f, 0f);
+        dragLayerAlpha.setDuration(217);
+        dragLayerAlpha.setInterpolator(Interpolators.LINEAR);
+        ObjectAnimator dragLayerTransY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, 0,
+                mDragLayerTransY);
+        dragLayerTransY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+        dragLayerTransY.setDuration(350);
+
+        hideLauncher.play(scrimAlpha);
+        hideLauncher.play(dragLayerAlpha);
+        hideLauncher.play(dragLayerTransY);
+        return hideLauncher;
+    }
+
+    private AnimatorSet getAppIconAnimator(View v) {
+        // Create a copy of the app icon
+        mFloatingView = new ImageView(mLauncher);
+        Bitmap iconBitmap = ((FastBitmapDrawable) ((BubbleTextView) v).getIcon()).getBitmap();
+        mFloatingView.setImageDrawable(new FastBitmapDrawable(iconBitmap));
+
+        // Position the copy of the app icon exactly on top of the original
+        Rect rect = new Rect();
+        mDragLayer.getDescendantRectRelativeToSelf(v, rect);
+        int viewLocationLeft = rect.left;
+        int viewLocationTop = rect.top;
+
+        ((BubbleTextView) v).getIconBounds(rect);
+        LayoutParams lp = new LayoutParams(rect.width(), rect.height());
+        lp.ignoreInsets = true;
+        lp.leftMargin = viewLocationLeft + rect.left;
+        lp.topMargin = viewLocationTop + rect.top;
+        mFloatingView.setLayoutParams(lp);
+
+        // Swap the two views in place.
+        ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView);
+        v.setVisibility(View.INVISIBLE);
+
+        AnimatorSet appIconAnimatorSet = new AnimatorSet();
+        // Animate the app icon to the center
+        float centerX = mDeviceProfile.widthPx / 2;
+        float centerY = mDeviceProfile.heightPx / 2;
+        float dX = centerX - lp.leftMargin - (lp.width / 2);
+        float dY = centerY - lp.topMargin - (lp.height / 2);
+        ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
+        ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
+
+        // Adjust the duration to change the "curve" of the app icon to the center.
+        boolean isBelowCenterY = lp.topMargin < centerY;
+        x.setDuration(isBelowCenterY ? 500 : 233);
+        y.setDuration(isBelowCenterY ? 233 : 500);
+        appIconAnimatorSet.play(x);
+        appIconAnimatorSet.play(y);
+
+        // Scale the app icon to take up the entire screen. This simplifies the math when
+        // animating the app window position / scale.
+        float maxScaleX = mDeviceProfile.widthPx / (float) rect.width();
+        float maxScaleY = mDeviceProfile.heightPx / (float) rect.height();
+        float scale = Math.max(maxScaleX, maxScaleY);
+        ObjectAnimator sX = ObjectAnimator.ofFloat(mFloatingView, View.SCALE_X, 1f, scale);
+        ObjectAnimator sY = ObjectAnimator.ofFloat(mFloatingView, View.SCALE_Y, 1f, scale);
+        sX.setDuration(500);
+        sY.setDuration(500);
+        appIconAnimatorSet.play(sX);
+        appIconAnimatorSet.play(sY);
+
+        // Fade out the app icon.
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
+        alpha.setStartDelay(17);
+        alpha.setDuration(33);
+        appIconAnimatorSet.play(alpha);
+
+        for (Animator a : appIconAnimatorSet.getChildAnimations()) {
+            a.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+        }
+        return appIconAnimatorSet;
+    }
+
+    private ValueAnimator getAppWindowAnimators(View v, RemoteAnimationTargetCompat[] targets) {
+        Rect iconBounds = new Rect();
+        ((BubbleTextView) v).getIconBounds(iconBounds);
+        int[] floatingViewBounds = new int[2];
+
+        Rect crop = new Rect();
+        Matrix matrix = new Matrix();
+
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(500);
+        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            boolean isFirstFrame = true;
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final float percent = animation.getAnimatedFraction();
+                final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent);
+
+                // Calculate app icon size.
+                float iconWidth = iconBounds.width() * mFloatingView.getScaleX();
+                float iconHeight = iconBounds.height() * mFloatingView.getScaleY();
+
+                // Scale the app window to match the icon size.
+                float scaleX = iconWidth / mDeviceProfile.widthPx;
+                float scaleY = iconHeight / mDeviceProfile.heightPx;
+                float scale = Math.min(1f, Math.min(scaleX, scaleY));
+                matrix.setScale(scale, scale);
+
+                // Position the scaled window on top of the icon
+                int deviceWidth = mDeviceProfile.widthPx;
+                int deviceHeight = mDeviceProfile.heightPx;
+                float scaledWindowWidth = deviceWidth * scale;
+                float scaledWindowHeight = deviceHeight * scale;
+
+                float offsetX = (scaledWindowWidth - iconWidth) / 2;
+                float offsetY = (scaledWindowHeight - iconHeight) / 2;
+                mFloatingView.getLocationInWindow(floatingViewBounds);
+                float transX0 = floatingViewBounds[0] - offsetX;
+                float transY0 = floatingViewBounds[1] - offsetY;
+                matrix.postTranslate(transX0, transY0);
+
+                // Fade in the app window.
+                float alphaDelay = 0;
+                float alphaDuration = 50;
+                float alpha = getValue(1f, 0f, alphaDelay, alphaDuration,
+                        appAnimator.getDuration() * percent, Interpolators.AGGRESSIVE_EASE);
+
+                // Animate the window crop so that it starts off as a square, and then reveals
+                // horizontally.
+                float cropHeight = deviceHeight * easePercent + deviceWidth * (1 - easePercent);
+                float initialTop = (deviceHeight - deviceWidth) / 2f;
+                crop.left = 0;
+                crop.top = (int) (initialTop * (1 - easePercent));
+                crop.right = deviceWidth;
+                crop.bottom = (int) (crop.top + cropHeight);
+
+                TransactionCompat t = new TransactionCompat();
+                for (RemoteAnimationTargetCompat target : targets) {
+                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                        t.setAlpha(target.leash, alpha);
+                        t.setMatrix(target.leash, matrix);
+                        t.setWindowCrop(target.leash, crop);
+                        Surface surface = getSurface(mFloatingView);
+                        t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
+                    }
+                    if (isFirstFrame) {
+                        t.show(target.leash);
+                    }
+                }
+                t.apply();
+
+                matrix.reset();
+                isFirstFrame = false;
+            }
+
+            /**
+             * Helper method that allows us to get interpolated values for embedded
+             * animations with a delay and/or different duration.
+             */
+            private float getValue(float start, float end, float delay, float duration,
+                                   float currentPlayTime, Interpolator i) {
+                float time = Math.max(0, currentPlayTime - delay);
+                float newPercent = Math.min(1f, time / duration);
+                newPercent = i.getInterpolation(newPercent);
+                return start * newPercent + end * (1 - newPercent);
+            }
+        });
+        return appAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index e848688..67a7d6a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -19,11 +19,14 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.os.Bundle;
+import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.widget.PopupMenu;
 import android.widget.Toast;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppTransitionManager;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
@@ -102,4 +105,8 @@
         RecentsView recents = launcher.getOverviewPanel();
         recents.reset();
     }
+
+    public static Bundle getActivityLaunchOptions(Launcher launcher, View v) {
+        return new LauncherAppTransitionManager(launcher).getActivityLauncherOptions(v);
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e3682b4..bfd3d69 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1913,29 +1913,7 @@
 
     @TargetApi(Build.VERSION_CODES.M)
     public Bundle getActivityLaunchOptions(View v) {
-        if (Utilities.ATLEAST_MARSHMALLOW) {
-            int left = 0, top = 0;
-            int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
-            if (v instanceof BubbleTextView) {
-                // Launch from center of icon, not entire view
-                Drawable icon = ((BubbleTextView) v).getIcon();
-                if (icon != null) {
-                    Rect bounds = icon.getBounds();
-                    left = (width - bounds.width()) / 2;
-                    top = v.getPaddingTop();
-                    width = bounds.width();
-                    height = bounds.height();
-                }
-            }
-            return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height).toBundle();
-        } else if (Utilities.ATLEAST_LOLLIPOP_MR1) {
-            // On L devices, we use the device default slide-up transition.
-            // On L MR1 devices, we use a custom version of the slide-up transition which
-            // doesn't have the delay present in the device default.
-            return ActivityOptions.makeCustomAnimation(
-                    this, R.anim.task_open_enter, R.anim.no_anim).toBundle();
-        }
-        return null;
+        return UiFactory.getActivityLaunchOptions(this, v);
     }
 
     public Rect getViewBounds(View v) {
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 8826e64..f3a3539 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -42,6 +42,8 @@
 
     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
 
+    public static final Interpolator AGGRESSIVE_EASE = new PathInterpolator(0.2f, 0f, 0f, 1f);
+
     /**
      * Inversion of zInterpolate, compounded with an ease-out.
      */
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
index 2ea10c2..d1b903c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -18,12 +18,20 @@
 
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
+import android.app.ActivityOptions;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
 import android.view.View.AccessibilityDelegate;
 
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.util.TouchController;
 
@@ -58,4 +66,30 @@
     }
 
     public static void resetOverview(Launcher launcher) { }
+
+    public static Bundle getActivityLaunchOptions(Launcher launcher, View v) {
+        if (Utilities.ATLEAST_MARSHMALLOW) {
+            int left = 0, top = 0;
+            int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
+            if (v instanceof BubbleTextView) {
+                // Launch from center of icon, not entire view
+                Drawable icon = ((BubbleTextView) v).getIcon();
+                if (icon != null) {
+                    Rect bounds = icon.getBounds();
+                    left = (width - bounds.width()) / 2;
+                    top = v.getPaddingTop();
+                    width = bounds.width();
+                    height = bounds.height();
+                }
+            }
+            return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height).toBundle();
+        } else if (Utilities.ATLEAST_LOLLIPOP_MR1) {
+            // On L devices, we use the device default slide-up transition.
+            // On L MR1 devices, we use a custom version of the slide-up transition which
+            // doesn't have the delay present in the device default.
+            return ActivityOptions.makeCustomAnimation(
+                    launcher, R.anim.task_open_enter, R.anim.no_anim).toBundle();
+        }
+        return null;
+    }
 }