Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 61110601 authored by Cyrus Boadway's avatar Cyrus Boadway
Browse files

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
parent c9867671
Loading
Loading
Loading
Loading
+140 −4
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@ import com.android.launcher3.util.MultiValueAlpha;
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.MultiValueUpdateListener;
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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener

    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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
        }
    }

    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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
            }
        });

        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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
                }
            });
        }

        animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
        return animatorSet;
        return backgroundRadiusAnim;
    }

    /**
@@ -1126,9 +1258,13 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
            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) {
+191 −0
Original line number Diff line number Diff line
/*
 * 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);
            }
        }
    }
}
+250 −0

File added.

Preview size limit exceeded, changes collapsed.

+20 −0
Original line number Diff line number Diff line
<?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" />