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

Commit bba6451b authored by Jon Miranda's avatar Jon Miranda
Browse files

Refactor app open animation so that FloatingIconView tracks window.

Before, the window used to track the FloatingIconView. With this refactor,
both app open and app close use the same update method.

With the refactor, we can now use adaptive icons to morph the icon into the
size of the window.

Movement/interpolators are still the same as the original except for the
alpha and the clip animation. To ensure a perfect tradeoff, we wait for the
icon to match the size and shape of the window before crossfading.
Currently it is set up so that the tradeoff happens when the animation is
15% done, but this can be tuned later.

Bug: 122843905

Change-Id: I7d3edbefffb15fe26958a62ab33cf23dc1203908
parent e3e1044a
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -113,8 +113,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
        final Rect iconLocation = new Rect();
        final FloatingIconView floatingView = workspaceView == null ? null
                : FloatingIconView.getFloatingIconView(activity, workspaceView,
                true /* hideOriginal */, false /* useDrawableAsIs */,
                activity.getDeviceProfile().getAspectRatioWithInsets(), iconLocation, null);
                true /* hideOriginal */, iconLocation, false /* isOpening */, null /* recycle */);

        return new HomeAnimationFactory() {
            @Nullable
+3 −2
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
import static com.android.launcher3.config.FeatureFlags.SWIPE_HOME;
import static com.android.launcher3.util.RaceConditionTracker.ENTER;
import static com.android.launcher3.util.RaceConditionTracker.EXIT;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
@@ -945,7 +946,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>

        // We want the window alpha to be 0 once this threshold is met, so that the
        // FolderIconView can be seen morphing into the icon shape.
        final float windowAlphaThreshold = isFloatingIconView ? 0.75f : 1f;
        final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
        anim.addOnUpdateListener((currentRect, progress) -> {
            float interpolatedProgress = Interpolators.ACCEL_1_5.getInterpolation(progress);

@@ -959,7 +960,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>

            if (isFloatingIconView) {
                ((FloatingIconView) floatingView).update(currentRect, iconAlpha, progress,
                        windowAlphaThreshold);
                        windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false);
            }

        });
+95 −146
Original line number Diff line number Diff line
@@ -20,15 +20,16 @@ import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
@@ -45,6 +46,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.CancellationSignal;
@@ -52,10 +54,8 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;

import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.InsettableFrameLayout.LayoutParams;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
@@ -103,13 +103,18 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
    private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
            "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";

    private static final int APP_LAUNCH_DURATION = 500;
    private static final long APP_LAUNCH_DURATION = 500;
    // Use a shorter duration for x or y translation to create a curve effect
    private static final int APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2;
    private static final long APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2;
    private static final long APP_LAUNCH_ALPHA_DURATION = 50;

    // We scale the durations for the downward app launch animations (minus the scale animation).
    private static final float APP_LAUNCH_DOWN_DUR_SCALE_FACTOR = 0.8f;
    private static final int APP_LAUNCH_ALPHA_START_DELAY = 32;
    private static final int APP_LAUNCH_ALPHA_DURATION = 50;
    private static final long APP_LAUNCH_DOWN_DURATION =
            (long) (APP_LAUNCH_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
    private static final long APP_LAUNCH_DOWN_CURVED_DURATION = APP_LAUNCH_DOWN_DURATION / 2;
    private static final long APP_LAUNCH_ALPHA_DOWN_DURATION =
            (long) (APP_LAUNCH_ALPHA_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);

    public static final int RECENTS_LAUNCH_DURATION = 336;
    private static final int LAUNCHER_RESUME_START_DELAY = 100;
@@ -207,11 +212,11 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans

            // Note that this duration is a guess as we do not know if the animation will be a
            // recents launch or not for sure until we know the opening app targets.
            int duration = fromRecents
            long duration = fromRecents
                    ? RECENTS_LAUNCH_DURATION
                    : APP_LAUNCH_DURATION;

            int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
            long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
                    - STATUS_BAR_TRANSITION_PRE_DELAY;
            return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
                    runner, duration, statusBarTransitionDelay));
@@ -266,7 +271,8 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
            }
            if (!isAllOpeningTargetTrs) break;
        }
        playIconAnimators(anim, v, windowTargetBounds, !isAllOpeningTargetTrs);
        anim.play(getOpeningWindowAnimators(v, targets, windowTargetBounds,
                !isAllOpeningTargetTrs));
        if (launcherClosing) {
            Pair<AnimatorSet, Runnable> launcherContentAnimator =
                    getLauncherContentAnimator(true /* isAppOpening */,
@@ -279,7 +285,6 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
                }
            });
        }
        anim.play(getOpeningWindowAnimators(v, targets, windowTargetBounds));
    }

    /**
@@ -398,165 +403,116 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
            float[] alphas, float[] trans);

    /**
     * Animators for the "floating view" of the view used to launch the target.
     * @return Animator that controls the window of the opening targets.
     */
    private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds,
            boolean toggleVisibility) {
        final boolean isBubbleTextView = v instanceof BubbleTextView;
        if (mFloatingView != null) {
            mFloatingView.setTranslationX(0);
            mFloatingView.setTranslationY(0);
            mFloatingView.setScaleX(1);
            mFloatingView.setScaleY(1);
            mFloatingView.setAlpha(1);
            mFloatingView.setBackground(null);
        }
        Rect rect = new Rect();
    private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets,
            Rect windowTargetBounds, boolean toggleVisibility) {
        Rect bounds = new Rect();
        mFloatingView = FloatingIconView.getFloatingIconView(mLauncher, v, toggleVisibility,
                true /* useDrawableAsIs */, -1 /* aspectRatio */, rect, mFloatingView);

        int viewLocationStart = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
        LayoutParams lp = (LayoutParams) mFloatingView.getLayoutParams();
        // Special RTL logic is needed to handle the window target bounds.
        lp.leftMargin = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
        mFloatingView.setLayoutParams(lp);

        int[] dragLayerBounds = new int[2];
        mDragLayer.getLocationOnScreen(dragLayerBounds);

        // Animate the app icon to the center of the window bounds in screen coordinates.
        float centerX = windowTargetBounds.centerX() - dragLayerBounds[0];
        float centerY = windowTargetBounds.centerY() - dragLayerBounds[1];

        float xPosition = mIsRtl
                ? windowTargetBounds.width() - lp.getMarginStart() - rect.width()
                : lp.getMarginStart();
        float dX = centerX - xPosition - (lp.width / 2f);
        float dY = centerY - lp.topMargin - (lp.height / 2f);

        ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
        ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
                bounds, true /* isOpening */, mFloatingView);
        Rect crop = new Rect();
        Matrix matrix = new Matrix();

        // Use upward animation for apps that are either on the bottom half of the screen, or are
        // relatively close to the center.
        boolean useUpwardAnimation = lp.topMargin > centerY
                || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx;
        if (useUpwardAnimation) {
            x.setDuration(APP_LAUNCH_CURVED_DURATION);
            y.setDuration(APP_LAUNCH_DURATION);
        } else {
            x.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_DURATION));
            y.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_CURVED_DURATION));
        }
        x.setInterpolator(AGGRESSIVE_EASE);
        y.setInterpolator(AGGRESSIVE_EASE);
        appOpenAnimator.play(x);
        appOpenAnimator.play(y);
        RemoteAnimationTargetSet openingTargets = new RemoteAnimationTargetSet(targets,
                MODE_OPENING);
        RemoteAnimationTargetSet closingTargets = new RemoteAnimationTargetSet(targets,
                MODE_CLOSING);
        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
                new SyncRtSurfaceTransactionApplierCompat(mFloatingView);

        // Scale the app icon to take up the entire screen. This simplifies the math when
        // animating the app window position / scale.
        float maxScaleX = windowTargetBounds.width() / (float) rect.width();
        float maxScaleY = windowTargetBounds.height() / (float) rect.height();
        float maxScaleX = windowTargetBounds.width() / (float) bounds.width();
        // We use windowTargetBounds.width for scaleY too since we start off the animation where the
        // window is clipped to a square.
        float maxScaleY = windowTargetBounds.width() / (float) bounds.height();
        float scale = Math.max(maxScaleX, maxScaleY);
        float startScale = 1f;
        if (isBubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
        if (v instanceof BubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
            Drawable dr = ((BubbleTextView) v).getIcon();
            if (dr instanceof FastBitmapDrawable) {
                startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
            }
        }
        final float initialStartScale = startScale;

        ObjectAnimator scaleAnim = ObjectAnimator
                .ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale);
        scaleAnim.setDuration(APP_LAUNCH_DURATION)
                .setInterpolator(Interpolators.EXAGGERATED_EASE);
        appOpenAnimator.play(scaleAnim);
        int[] dragLayerBounds = new int[2];
        mDragLayer.getLocationOnScreen(dragLayerBounds);

        // Fade out the app icon.
        ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
        if (useUpwardAnimation) {
            alpha.setStartDelay(APP_LAUNCH_ALPHA_START_DELAY);
            alpha.setDuration(APP_LAUNCH_ALPHA_DURATION);
        } else {
            alpha.setStartDelay((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR
                    * APP_LAUNCH_ALPHA_START_DELAY));
            alpha.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_ALPHA_DURATION));
        }
        alpha.setInterpolator(LINEAR);
        appOpenAnimator.play(alpha);
        // Animate the app icon to the center of the window bounds in screen coordinates.
        float centerX = windowTargetBounds.centerX() - dragLayerBounds[0];
        float centerY = windowTargetBounds.centerY() - dragLayerBounds[1];

        float dX = centerX - bounds.centerX();
        float dY = centerY - bounds.centerY();

        boolean useUpwardAnimation = bounds.top > centerY
                || Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx;
        final long xDuration = useUpwardAnimation ? APP_LAUNCH_CURVED_DURATION
                : APP_LAUNCH_DOWN_DURATION;
        final long yDuration = useUpwardAnimation ? APP_LAUNCH_DURATION
                : APP_LAUNCH_DOWN_CURVED_DURATION;
        final long alphaDuration = useUpwardAnimation ? APP_LAUNCH_ALPHA_DURATION
                : APP_LAUNCH_ALPHA_DOWN_DURATION;

        RectF targetBounds = new RectF(windowTargetBounds);
        RectF currentBounds = new RectF();
        RectF temp = new RectF();

        appOpenAnimator.addListener(mFloatingView);
        appOpenAnimator.addListener(new AnimatorListenerAdapter() {
        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
        appAnimator.setDuration(APP_LAUNCH_DURATION);
        appAnimator.setInterpolator(LINEAR);
        appAnimator.addListener(mFloatingView);
        appAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // Reset launcher to normal state
                if (isBubbleTextView) {
                if (v instanceof BubbleTextView) {
                    ((BubbleTextView) v).setStayPressed(false);
                }
                v.setVisibility(View.VISIBLE);
                ((ViewGroup) mDragLayer.getParent()).getOverlay().remove(mFloatingView);
            }
        });
    }

    /**
     * @return Animator that controls the window of the opening targets.
     */
    private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets,
            Rect windowTargetBounds) {
        Rect bounds = new Rect();
        if (v.getParent() instanceof DeepShortcutView) {
            // Deep shortcut views have their icon drawn in a separate view.
            DeepShortcutView view = (DeepShortcutView) v.getParent();
            mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), bounds);
        } else if (v instanceof BubbleTextView) {
            ((BubbleTextView) v).getIconBounds(bounds);
        } else {
            mDragLayer.getDescendantRectRelativeToSelf(v, bounds);
        }
        int[] floatingViewBounds = new int[2];

        Rect crop = new Rect();
        Matrix matrix = new Matrix();

        RemoteAnimationTargetSet openingTargets = new RemoteAnimationTargetSet(targets,
                MODE_OPENING);
        RemoteAnimationTargetSet closingTargets = new RemoteAnimationTargetSet(targets,
                MODE_CLOSING);
        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
                new SyncRtSurfaceTransactionApplierCompat(mFloatingView);

        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
        appAnimator.setDuration(APP_LAUNCH_DURATION);
        float shapeRevealDuration = APP_LAUNCH_DURATION * SHAPE_PROGRESS_DURATION;
        appAnimator.addUpdateListener(new MultiValueUpdateListener() {
            // Fade alpha for the app window.
            FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR);
            FloatProp mDx = new FloatProp(0, dX, 0, xDuration, AGGRESSIVE_EASE);
            FloatProp mDy = new FloatProp(0, dY, 0, yDuration, AGGRESSIVE_EASE);
            FloatProp mIconScale = new FloatProp(initialStartScale, scale, 0, APP_LAUNCH_DURATION,
                    EXAGGERATED_EASE);
            FloatProp mIconAlpha = new FloatProp(1f, 0f, shapeRevealDuration, alphaDuration,
                    LINEAR);
            FloatProp mCropHeight = new FloatProp(windowTargetBounds.width(),
                    windowTargetBounds.height(), 0, shapeRevealDuration, AGGRESSIVE_EASE);

            @Override
            public void onUpdate(float percent) {
                final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent);

                // Calculate app icon size.
                float iconWidth = bounds.width() * mFloatingView.getScaleX();
                float iconHeight = bounds.height() * mFloatingView.getScaleY();
                float iconWidth = bounds.width() * mIconScale.value;
                float iconHeight = bounds.height() * mIconScale.value;

                // Animate the window crop so that it starts off as a square, and then reveals
                // horizontally.
                int windowWidth = windowTargetBounds.width();
                int windowHeight = (int) mCropHeight.value;
                crop.set(0, 0, windowWidth, windowHeight);

                // Scale the app window to match the icon size.
                float scaleX = iconWidth / windowTargetBounds.width();
                float scaleY = iconHeight / windowTargetBounds.height();
                float scale = Math.min(1f, Math.min(scaleX, scaleY));
                float scaleX = iconWidth / windowWidth;
                float scaleY = iconHeight / windowHeight;
                float scale = Math.min(1f, Math.max(scaleX, scaleY));

                // Position the scaled window on top of the icon
                int windowWidth = windowTargetBounds.width();
                int windowHeight = windowTargetBounds.height();
                float scaledWindowWidth = windowWidth * scale;
                float scaledWindowHeight = windowHeight * scale;

                float offsetX = (scaledWindowWidth - iconWidth) / 2;
                float offsetY = (scaledWindowHeight - iconHeight) / 2;
                mFloatingView.getLocationOnScreen(floatingViewBounds);

                float transX0 = floatingViewBounds[0] - offsetX;
                float transY0 = floatingViewBounds[1] - offsetY;
                // Calculate the window position
                temp.set(bounds);
                temp.offset(dragLayerBounds[0], dragLayerBounds[1]);
                temp.offset(mDx.value, mDy.value);
                Utilities.scaleRectFAboutCenter(temp, mIconScale.value);
                float transX0 = temp.left - offsetX;
                float transY0 = temp.top - offsetY;

                float windowRadius = 0;
                if (!mDeviceProfile.isMultiWindowMode &&
@@ -565,19 +521,9 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
                            .getWindowCornerRadius();
                }

                // Animate the window crop so that it starts off as a square, and then reveals
                // horizontally.
                float cropHeight = windowHeight * easePercent + windowWidth * (1 - easePercent);
                float initialTop = (windowHeight - windowWidth) / 2f;
                crop.left = 0;
                crop.top = (int) (initialTop * (1 - easePercent));
                crop.right = windowWidth;
                crop.bottom = (int) (crop.top + cropHeight);

                SurfaceParams[] params = new SurfaceParams[targets.length];
                for (int i = targets.length - 1; i >= 0; i--) {
                    RemoteAnimationTargetCompat target = targets[i];

                    Rect targetCrop;
                    final float alpha;
                    final float cornerRadius;
@@ -585,12 +531,15 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
                        matrix.setScale(scale, scale);
                        matrix.postTranslate(transX0, transY0);
                        targetCrop = crop;
                        alpha = mAlpha.value;
                        alpha = 1f - mIconAlpha.value;
                        cornerRadius = windowRadius;
                        matrix.mapRect(currentBounds, targetBounds);
                        mFloatingView.update(currentBounds, mIconAlpha.value, percent, 0f,
                                cornerRadius * scale, true /* isOpening */);
                    } else {
                        matrix.setTranslate(target.position.x, target.position.y);
                        alpha = 1f;
                        targetCrop = target.sourceContainerBounds;
                        alpha = 1f;
                        cornerRadius = 0;
                    }

+4 −7
Original line number Diff line number Diff line
@@ -51,6 +51,9 @@ public class DeviceProfile {
    public final int heightPx;
    public final int availableWidthPx;
    public final int availableHeightPx;

    public final float aspectRatio;

    /**
     * The maximum amount of left/right workspace padding as a percentage of the screen width.
     * To be clear, this means that up to 7% of the screen width can be used as left padding, and
@@ -160,7 +163,7 @@ public class DeviceProfile {
        isTablet = res.getBoolean(R.bool.is_tablet);
        isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
        isPhone = !isTablet && !isLargeTablet;
        float aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
        aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
        boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;

        // Some more constants
@@ -618,12 +621,6 @@ public class DeviceProfile {
        }
    }

    public float getAspectRatioWithInsets() {
        int w = widthPx - mInsets.left - mInsets.right;
        int h = heightPx - mInsets.top - mInsets.bottom;
        return ((float) Math.max(w, h)) / Math.min(w, h);
    }

    private static Context getContext(Context c, int orientation) {
        Configuration context = new Configuration(c.getResources().getConfiguration());
        context.orientation = orientation;
+99 −68

File changed.

Preview size limit exceeded, changes collapsed.