Animate App Widget activity launch from Quickstep launchers

Following the general pattern on icon launch animations, define a
'floating widget view' to represent the appearance of the app widget
through the launch animation.

The floating widget view separates the foreground and background
components of the LauncherAppWidgetHostView, which can be positioned
and animated separately.

The background (or placeholder if no background can be identified)
moves and grows from its original position and size to the launched
app's position and size.

The widget's foreground scales and moves to match, fading out to be
replaced with the app window.

Bug: 169042867
Test: manual
Change-Id: I65d2b1bc80275f9df460790720e13d1650093347
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 827eb7d..b38e48d 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -79,6 +79,7 @@
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskViewUtils;
@@ -86,6 +87,7 @@
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.views.FloatingWidgetView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityCompat;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -160,6 +162,9 @@
 
     private static final int MAX_NUM_TASKS = 5;
 
+    // Cross-fade duration between App Widget and App
+    private static final int WIDGET_CROSSFADE_DURATION_MILLIS = 125;
+
     protected final BaseQuickstepLauncher mLauncher;
 
     private final DragLayer mDragLayer;
@@ -351,6 +356,29 @@
         }
     }
 
+    private void composeWidgetLaunchAnimator(
+            @NonNull AnimatorSet anim,
+            @NonNull LauncherAppWidgetHostView v,
+            @NonNull RemoteAnimationTargetCompat[] appTargets,
+            @NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
+            @NonNull RemoteAnimationTargetCompat[] nonAppTargets) {
+        mLauncher.getStateManager().setCurrentAnimation(anim);
+
+        Rect windowTargetBounds = getWindowTargetBounds(appTargets, getRotationChange(appTargets));
+        anim.play(getOpeningWindowAnimatorsForWidget(v, appTargets, wallpaperTargets, nonAppTargets,
+                windowTargetBounds, areAllTargetsTranslucent(appTargets)));
+
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mLauncher.addOnResumeCallback(() ->
+                        ObjectAnimator.ofFloat(mLauncher.getDepthController(), DEPTH,
+                                mLauncher.getStateManager().getState().getDepth(
+                                        mLauncher)).start());
+            }
+        });
+    }
+
     /**
      * Return the window bounds of the opening target.
      * In multiwindow mode, we need to get the final size of the opening app window target to help
@@ -744,6 +772,112 @@
             }
         });
 
+        animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+        return animatorSet;
+    }
+
+    private Animator getOpeningWindowAnimatorsForWidget(LauncherAppWidgetHostView v,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets, Rect windowTargetBounds,
+            boolean appTargetsAreTranslucent) {
+        final RectF widgetBackgroundBounds = new RectF();
+        final Rect appWindowCrop = new Rect();
+        final Matrix matrix = new Matrix();
+
+        final float finalWindowRadius = mDeviceProfile.isMultiWindowMode
+                ? 0 : getWindowCornerRadius(mLauncher.getResources());
+        final FloatingWidgetView floatingView = FloatingWidgetView.getFloatingWidgetView(mLauncher,
+                v, widgetBackgroundBounds, windowTargetBounds, finalWindowRadius);
+        final float initialWindowRadius = supportsRoundedCornersOnWindows(mLauncher.getResources())
+                ? floatingView.getInitialCornerRadius() : 0;
+
+        RemoteAnimationTargets openingTargets = new RemoteAnimationTargets(appTargets,
+                wallpaperTargets, nonAppTargets, MODE_OPENING);
+        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(floatingView);
+        openingTargets.addReleaseCheck(surfaceApplier);
+
+        AnimatorSet animatorSet = new AnimatorSet();
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(APP_LAUNCH_DURATION);
+        appAnimator.setInterpolator(LINEAR);
+        appAnimator.addListener(floatingView);
+        appAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                openingTargets.release();
+            }
+        });
+        floatingView.setFastFinishRunnable(animatorSet::end);
+
+        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
+            float mAppWindowScale = 1;
+            final FloatProp mWidgetForegroundAlpha = new FloatProp(1 /* start */,
+                    0 /* end */, 0 /* delay */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+            final FloatProp mWidgetFallbackBackgroundAlpha = new FloatProp(0 /* start */,
+                    1 /* end */, 0 /* delay */, 75 /* duration */, LINEAR);
+            final FloatProp mPreviewAlpha = new FloatProp(0 /* start */, 1 /* end */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* delay */,
+                    WIDGET_CROSSFADE_DURATION_MILLIS / 2 /* duration */, LINEAR);
+            final FloatProp mWindowRadius = new FloatProp(initialWindowRadius, finalWindowRadius,
+                    0 /* start */, RADIUS_DURATION, LINEAR);
+            final FloatProp mCornerRadiusProgress = new FloatProp(0, 1, 0, RADIUS_DURATION, LINEAR);
+
+            // Window & widget background positioning bounds
+            final FloatProp mDx = new FloatProp(widgetBackgroundBounds.centerX(),
+                    windowTargetBounds.centerX(), 0 /* delay */, APP_LAUNCH_CURVED_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mDy = new FloatProp(widgetBackgroundBounds.centerY(),
+                    windowTargetBounds.centerY(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mWidth = new FloatProp(widgetBackgroundBounds.width(),
+                    windowTargetBounds.width(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+            final FloatProp mHeight = new FloatProp(widgetBackgroundBounds.height(),
+                    windowTargetBounds.height(), 0 /* delay */, APP_LAUNCH_DURATION,
+                    EXAGGERATED_EASE);
+
+            @Override
+            public void onUpdate(float percent) {
+                widgetBackgroundBounds.set(mDx.value - mWidth.value / 2f,
+                        mDy.value - mHeight.value / 2f, mDx.value + mWidth.value / 2f,
+                        mDy.value + mHeight.value / 2f);
+                // Set app window scaling factor to match widget background width
+                mAppWindowScale = widgetBackgroundBounds.width() / windowTargetBounds.width();
+                // Crop scaled app window to match widget
+                appWindowCrop.set(0 /* left */, 0 /* top */,
+                        Math.round(windowTargetBounds.width()) /* right */,
+                        Math.round(widgetBackgroundBounds.height() / mAppWindowScale) /* bottom */);
+                matrix.setTranslate(widgetBackgroundBounds.left, widgetBackgroundBounds.top);
+                matrix.postScale(mAppWindowScale, mAppWindowScale, widgetBackgroundBounds.left,
+                        widgetBackgroundBounds.top);
+
+                SurfaceParams[] params = new SurfaceParams[appTargets.length];
+                float floatingViewAlpha = appTargetsAreTranslucent ? 1 - mPreviewAlpha.value : 1;
+                for (int i = appTargets.length - 1; i >= 0; i--) {
+                    RemoteAnimationTargetCompat target = appTargets[i];
+                    SurfaceParams.Builder builder = new SurfaceParams.Builder(target.leash);
+                    if (target.mode == MODE_OPENING) {
+                        floatingView.update(widgetBackgroundBounds, floatingViewAlpha,
+                                mWidgetForegroundAlpha.value, mWidgetFallbackBackgroundAlpha.value,
+                                mCornerRadiusProgress.value);
+                        builder.withMatrix(matrix)
+                                .withWindowCrop(appWindowCrop)
+                                .withAlpha(mPreviewAlpha.value)
+                                .withCornerRadius(mWindowRadius.value / mAppWindowScale);
+                    }
+                    params[i] = builder.build();
+                }
+                surfaceApplier.scheduleApply(params);
+            }
+        });
+
+        animatorSet.playTogether(appAnimator, getBackgroundAnimator(appTargets));
+        return animatorSet;
+    }
+
+    private ObjectAnimator getBackgroundAnimator(RemoteAnimationTargetCompat[] appTargets) {
         // When launching an app from overview that doesn't map to a task, we still want to just
         // blur the wallpaper instead of the launcher surface as well
         boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
@@ -761,9 +895,7 @@
                 }
             });
         }
-
-        animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
-        return animatorSet;
+        return backgroundRadiusAnim;
     }
 
     /**
@@ -1126,9 +1258,13 @@
             boolean launcherClosing =
                     launcherIsATargetWithMode(appTargets, MODE_CLOSING);
 
+            final boolean launchingFromWidget = mV instanceof LauncherAppWidgetHostView;
             final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
             final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
-            if (launchingFromRecents) {
+            if (launchingFromWidget) {
+                composeWidgetLaunchAnimator(anim, (LauncherAppWidgetHostView) mV, appTargets,
+                        wallpaperTargets, nonAppTargets);
+            } else if (launchingFromRecents) {
                 composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets, nonAppTargets,
                         launcherClosing);
             } else if (launchingFromTaskbar) {
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
new file mode 100644
index 0000000..f74aa55
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetBackgroundView.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2021 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.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews.RemoteViewOutlineProvider;
+
+import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+import java.util.stream.IntStream;
+
+/**
+ * Mimics the appearance of the background view of a {@link LauncherAppWidgetHostView} through a
+ * an App Widget activity launch animation.
+ */
+@TargetApi(Build.VERSION_CODES.S)
+final class FloatingWidgetBackgroundView extends View {
+    private final ColorDrawable mFallbackDrawable = new ColorDrawable();
+    private final DrawableProperties mForegroundProperties = new DrawableProperties();
+    private final DrawableProperties mBackgroundProperties = new DrawableProperties();
+
+    private Drawable mOriginalForeground;
+    private Drawable mOriginalBackground;
+    private float mFinalRadius;
+    private float mInitialOutlineRadius;
+    private float mOutlineRadius;
+    private boolean mIsUsingFallback;
+    private View mSourceView;
+
+    FloatingWidgetBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
+            }
+        });
+        setClipToOutline(true);
+    }
+
+    void init(LauncherAppWidgetHostView hostView, View backgroundView, float finalRadius) {
+        mFinalRadius = finalRadius;
+        mSourceView = backgroundView;
+        mInitialOutlineRadius = getOutlineRadius(hostView, backgroundView);
+        mIsUsingFallback = false;
+        if (isSupportedDrawable(backgroundView.getForeground())) {
+            mOriginalForeground = backgroundView.getForeground();
+            mForegroundProperties.init(
+                    mOriginalForeground.getConstantState().newDrawable().mutate());
+            setForeground(mForegroundProperties.mDrawable);
+            mSourceView.setForeground(null);
+        }
+        if (isSupportedDrawable(backgroundView.getBackground())) {
+            mOriginalBackground = backgroundView.getBackground();
+            mBackgroundProperties.init(
+                    mOriginalBackground.getConstantState().newDrawable().mutate());
+            setBackground(mBackgroundProperties.mDrawable);
+            mSourceView.setBackground(null);
+        } else if (mOriginalForeground == null) {
+            mFallbackDrawable.setColor(Themes.getColorBackground(backgroundView.getContext()));
+            setBackground(mFallbackDrawable);
+            mIsUsingFallback = true;
+        }
+    }
+
+    /** Update the animated properties of the drawables. */
+    void update(float cornerRadiusProgress, float fallbackAlpha) {
+        if (isUninitialized()) return;
+        mOutlineRadius = mInitialOutlineRadius + (mFinalRadius - mInitialOutlineRadius)
+                * cornerRadiusProgress;
+        mForegroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+        mBackgroundProperties.updateDrawable(mFinalRadius, cornerRadiusProgress);
+        setAlpha(mIsUsingFallback ? fallbackAlpha : 1f);
+    }
+
+    /** Restores the drawables to the source view. */
+    void finish() {
+        if (isUninitialized()) return;
+        mSourceView.setForeground(mOriginalForeground);
+        mSourceView.setBackground(mOriginalBackground);
+    }
+
+    void recycle() {
+        mSourceView = null;
+        mOriginalForeground = null;
+        mOriginalBackground = null;
+        mOutlineRadius = 0;
+        mFinalRadius = 0;
+        setForeground(null);
+        setBackground(null);
+    }
+
+    /** Get the largest of drawable corner radii or background view outline radius. */
+    float getMaximumRadius() {
+        if (isUninitialized()) return 0;
+        return Math.max(mInitialOutlineRadius, Math.max(getMaxRadius(mOriginalForeground),
+                getMaxRadius(mOriginalBackground)));
+    }
+
+    private boolean isUninitialized() {
+        return mSourceView == null;
+    }
+
+    /** Returns the maximum corner radius of {@param drawable}. */
+    private static float getMaxRadius(Drawable drawable) {
+        if (!(drawable instanceof GradientDrawable)) return 0;
+        float[] cornerRadii = ((GradientDrawable) drawable).getCornerRadii();
+        float cornerRadius = ((GradientDrawable) drawable).getCornerRadius();
+        double radiiMax = cornerRadii == null ? 0 : IntStream.range(0, cornerRadii.length)
+                .mapToDouble(i -> cornerRadii[i]).max().orElse(0);
+        return Math.max(cornerRadius, (float) radiiMax);
+    }
+
+    /** Returns whether the given drawable type is supported. */
+    private static boolean isSupportedDrawable(Drawable drawable) {
+        return drawable instanceof ColorDrawable || (drawable instanceof GradientDrawable
+                && ((GradientDrawable) drawable).getShape() == GradientDrawable.RECTANGLE);
+    }
+
+    /** Corner radius from source view's outline, or enforced view. */
+    private static float getOutlineRadius(LauncherAppWidgetHostView hostView, View v) {
+        if (RoundedCornerEnforcement.isRoundedCornerEnabled()
+                && hostView.hasEnforcedCornerRadius()) {
+            return hostView.getEnforcedCornerRadius();
+        } else if (v.getOutlineProvider() instanceof RemoteViewOutlineProvider
+                && v.getClipToOutline()) {
+            return ((RemoteViewOutlineProvider) v.getOutlineProvider()).getRadius();
+        }
+        return 0;
+    }
+
+    /** Stores and modifies a drawable's properties through an animation. */
+    private static class DrawableProperties {
+        private Drawable mDrawable;
+        private float mOriginalRadius;
+        private float[] mOriginalRadii;
+        private final float[] mTmpRadii = new float[8];
+
+        /** Store a drawable's animated properties. */
+        void init(Drawable drawable) {
+            mDrawable = drawable;
+            if (!(drawable instanceof GradientDrawable)) return;
+            mOriginalRadius = ((GradientDrawable) drawable).getCornerRadius();
+            mOriginalRadii = ((GradientDrawable) drawable).getCornerRadii();
+        }
+
+        /**
+         * Update the drawable for the given animation state.
+         *
+         * @param finalRadius the radius of each corner when {@param progress} is 1
+         * @param progress    the linear progress of the corner radius from its original value to
+         *                    {@param finalRadius}
+         */
+        void updateDrawable(float finalRadius, float progress) {
+            if (!(mDrawable instanceof GradientDrawable)) return;
+            GradientDrawable d = (GradientDrawable) mDrawable;
+            if (mOriginalRadii != null) {
+                for (int i = 0; i < mOriginalRadii.length; i++) {
+                    mTmpRadii[i] = mOriginalRadii[i] + (finalRadius - mOriginalRadii[i]) * progress;
+                }
+                d.setCornerRadii(mTmpRadii);
+            } else {
+                d.setCornerRadius(mOriginalRadius + (finalRadius - mOriginalRadius) * progress);
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
new file mode 100644
index 0000000..d23884c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingWidgetView.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2021 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.views;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.GhostView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.views.ListenerView;
+import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.RoundedCornerEnforcement;
+
+/** A view that mimics an App Widget through a launch animation. */
+@TargetApi(Build.VERSION_CODES.S)
+public class FloatingWidgetView extends FrameLayout implements AnimatorListener {
+    private static final Matrix sTmpMatrix = new Matrix();
+
+    private final Launcher mLauncher;
+    private final ListenerView mListenerView;
+    private final FloatingWidgetBackgroundView mBackgroundView;
+    private final RectF mBackgroundOffset = new RectF();
+
+    private LauncherAppWidgetHostView mAppWidgetView;
+    private View mAppWidgetBackgroundView;
+    private RectF mBackgroundPosition;
+    private GhostView mForegroundOverlayView;
+
+    private Runnable mEndRunnable;
+    private Runnable mFastFinishRunnable;
+
+    public FloatingWidgetView(Context context) {
+        this(context, null);
+    }
+
+    public FloatingWidgetView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FloatingWidgetView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        mListenerView = new ListenerView(context, attrs);
+        mBackgroundView = new FloatingWidgetBackgroundView(context, attrs, defStyleAttr);
+        addView(mBackgroundView);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animator) {
+        Runnable endRunnable = mEndRunnable;
+        mEndRunnable = null;
+        if (endRunnable != null) {
+            endRunnable.run();
+        }
+    }
+
+    @Override
+    public void onAnimationStart(Animator animator) {
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animator) {
+    }
+
+    @Override
+    public void onAnimationRepeat(Animator animator) {
+    }
+
+    /** Sets a runnable that is called after a call to {@link #fastFinish()}. */
+    public void setFastFinishRunnable(Runnable runnable) {
+        mFastFinishRunnable = runnable;
+    }
+
+    /** Callback at the end or early exit of the animation. */
+    public void fastFinish() {
+        if (isUninitialized()) return;
+        Runnable fastFinishRunnable = mFastFinishRunnable;
+        if (fastFinishRunnable != null) {
+            fastFinishRunnable.run();
+        }
+        Runnable endRunnable = mEndRunnable;
+        mEndRunnable = null;
+        if (endRunnable != null) {
+            endRunnable.run();
+        }
+    }
+
+    private void init(DragLayer dragLayer, LauncherAppWidgetHostView originalView,
+            RectF widgetBackgroundPosition, Rect windowTargetBounds, float windowCornerRadius) {
+        mAppWidgetView = originalView;
+        mAppWidgetView.beginDeferringUpdates();
+        mBackgroundPosition = widgetBackgroundPosition;
+        mEndRunnable = () -> finish(dragLayer);
+
+        mAppWidgetBackgroundView = RoundedCornerEnforcement.findBackground(mAppWidgetView);
+        if (mAppWidgetBackgroundView == null) {
+            mAppWidgetBackgroundView = mAppWidgetView;
+        }
+
+        getRelativePosition(mAppWidgetBackgroundView, dragLayer, mBackgroundPosition);
+        getRelativePosition(mAppWidgetBackgroundView, mAppWidgetView, mBackgroundOffset);
+        mBackgroundView.init(mAppWidgetView, mAppWidgetBackgroundView, windowCornerRadius);
+        // Layout call before GhostView creation so that the overlaid view isn't clipped
+        layout(0, 0, windowTargetBounds.width(), windowTargetBounds.height());
+        mForegroundOverlayView = GhostView.addGhost(mAppWidgetView, this);
+        positionViews();
+
+        mListenerView.setListener(this::fastFinish);
+        dragLayer.addView(mListenerView);
+    }
+
+    /**
+     * Updates the position and opacity of the floating widget's components.
+     *
+     * @param backgroundPosition      the new position of the widget's background relative to the
+     *                                {@link FloatingWidgetView}'s parent
+     * @param floatingWidgetAlpha     the overall opacity of the {@link FloatingWidgetView}
+     * @param foregroundAlpha         the opacity of the foreground layer
+     * @param fallbackBackgroundAlpha the opacity of the fallback background used when the App
+     *                                Widget doesn't have a background
+     * @param cornerRadiusProgress    progress of the corner radius animation, where 0 is the
+     *                                original radius and 1 is the window radius
+     */
+    public void update(RectF backgroundPosition, float floatingWidgetAlpha, float foregroundAlpha,
+            float fallbackBackgroundAlpha, float cornerRadiusProgress) {
+        if (isUninitialized()) return;
+        setAlpha(floatingWidgetAlpha);
+        mBackgroundView.update(cornerRadiusProgress, fallbackBackgroundAlpha);
+        mAppWidgetView.setAlpha(foregroundAlpha);
+        mBackgroundPosition = backgroundPosition;
+        positionViews();
+    }
+
+    /** Sets the layout parameters of the floating view and its background view child. */
+    private void positionViews() {
+        LayoutParams layoutParams = (LayoutParams) getLayoutParams();
+        layoutParams.setMargins(0, 0, 0, 0);
+        setLayoutParams(layoutParams);
+
+        // FloatingWidgetView layout is forced LTR
+        mBackgroundView.setTranslationX(mBackgroundPosition.left);
+        mBackgroundView.setTranslationY(mBackgroundPosition.top);
+        LayoutParams backgroundParams = (LayoutParams) mBackgroundView.getLayoutParams();
+        backgroundParams.leftMargin = 0;
+        backgroundParams.topMargin = 0;
+        backgroundParams.width = (int) mBackgroundPosition.width();
+        backgroundParams.height = (int) mBackgroundPosition.height();
+        mBackgroundView.setLayoutParams(backgroundParams);
+
+        sTmpMatrix.reset();
+        float foregroundScale = mBackgroundPosition.width() / mAppWidgetBackgroundView.getWidth();
+        sTmpMatrix.setTranslate(-mBackgroundOffset.left - mAppWidgetView.getLeft(),
+                -mBackgroundOffset.top - mAppWidgetView.getTop());
+        sTmpMatrix.postScale(foregroundScale, foregroundScale);
+        sTmpMatrix.postTranslate(mBackgroundPosition.left, mBackgroundPosition.top);
+        mForegroundOverlayView.setMatrix(sTmpMatrix);
+    }
+
+    private void finish(DragLayer dragLayer) {
+        mAppWidgetView.setAlpha(1f);
+        GhostView.removeGhost(mAppWidgetView);
+        ((ViewGroup) dragLayer.getParent()).removeView(this);
+        dragLayer.removeView(mListenerView);
+        mBackgroundView.finish();
+        mAppWidgetView.endDeferringUpdates();
+        recycle();
+        mLauncher.getViewCache().recycleView(R.layout.floating_widget_view, this);
+    }
+
+    public float getInitialCornerRadius() {
+        return mBackgroundView.getMaximumRadius();
+    }
+
+    private boolean isUninitialized() {
+        return mForegroundOverlayView == null;
+    }
+
+    private void recycle() {
+        mEndRunnable = null;
+        mFastFinishRunnable = null;
+        mBackgroundPosition = null;
+        mListenerView.setListener(null);
+        mAppWidgetView = null;
+        mForegroundOverlayView = null;
+        mAppWidgetBackgroundView = null;
+        mBackgroundView.recycle();
+    }
+
+    /**
+     * Configures and returns a an instance of {@link FloatingWidgetView} matching the appearance of
+     * {@param originalView}.
+     *
+     * @param widgetBackgroundPosition a {@link RectF} that will be updated with the widget's
+     *                                 background bounds
+     * @param windowTargetBounds       the bounds of the window when launched
+     * @param windowCornerRadius       the corner radius of the window
+     */
+    public static FloatingWidgetView getFloatingWidgetView(Launcher launcher,
+            LauncherAppWidgetHostView originalView, RectF widgetBackgroundPosition,
+            Rect windowTargetBounds, float windowCornerRadius) {
+        final DragLayer dragLayer = launcher.getDragLayer();
+        ViewGroup parent = (ViewGroup) dragLayer.getParent();
+        FloatingWidgetView floatingView =
+                launcher.getViewCache().getView(R.layout.floating_widget_view, launcher, parent);
+        floatingView.recycle();
+
+        floatingView.init(dragLayer, originalView, widgetBackgroundPosition, windowTargetBounds,
+                windowCornerRadius);
+        parent.addView(floatingView);
+        return floatingView;
+    }
+
+    private static void getRelativePosition(View descendant, View ancestor, RectF position) {
+        float[] points = new float[]{0, 0, descendant.getWidth(), descendant.getHeight()};
+        Utilities.getDescendantCoordRelativeToAncestor(descendant, ancestor, points,
+                false /* includeRootScroll */);
+        position.set(
+                Math.min(points[0], points[2]),
+                Math.min(points[1], points[3]),
+                Math.max(points[0], points[2]),
+                Math.max(points[1], points[3]));
+    }
+}
diff --git a/res/layout/floating_widget_view.xml b/res/layout/floating_widget_view.xml
new file mode 100644
index 0000000..eea7a92
--- /dev/null
+++ b/res/layout/floating_widget_view.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<com.android.quickstep.views.FloatingWidgetView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layoutDirection="ltr" />