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

Commit b796cfec authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Add generic support for corner radius in launch animations.

This CL adds a generic way in GhostViewLaunchAnimationController to
change the corner radius of the expanding background. It does so by
supporting Gradient- and LayerDrawable (and therefore RippleDrawable).

This will be first used to animate the launch of the settings after long
clicking a QS tile (see ag/13988954), but this implementation will
hopefully work with other views as well.

This CL also adds the wiring in ActivityStarter that will be needed by
the QuickSettings.

Bug: 184121838
Test: Manual
Change-Id: I40f7048edd682d5d68c32b4439368d19009a8df1
parent 7d830825
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -63,6 +63,8 @@ public interface ActivityStarter {
    void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade);
    void startActivity(Intent intent, boolean dismissShade, Callback callback);
    void postStartActivityDismissingKeyguard(Intent intent, int delay);
    void postStartActivityDismissingKeyguard(Intent intent, int delay,
            @Nullable ActivityLaunchAnimator.Controller animationController);
    void postStartActivityDismissingKeyguard(PendingIntent intent);

    /**
+2 −2
Original line number Diff line number Diff line
@@ -35,12 +35,12 @@ class ActivityLaunchAnimator {
        private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
        private const val ANIMATION_DELAY_NAV_FADE_IN =
                ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN
        private const val LAUNCH_TIMEOUT = 500L
        private const val LAUNCH_TIMEOUT = 1000L

        // TODO(b/184121838): Use android.R.interpolator.fast_out_extra_slow_in instead.
        // TODO(b/184121838): Move com.android.systemui.Interpolators in an animation library we can
        // reuse here.
        private val ANIMATION_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
        private val ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f)
        private val LINEAR_INTERPOLATOR = LinearInterpolator()
        private val ALPHA_IN_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
        private val ALPHA_OUT_INTERPOLATOR = PathInterpolator(0f, 0f, 0.8f, 1f)
+140 −7
Original line number Diff line number Diff line
@@ -7,6 +7,8 @@ import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.view.GhostView
import android.view.View
import android.view.ViewGroup
@@ -66,15 +68,28 @@ open class GhostedViewLaunchAnimatorController(
        topCornerRadius: Float,
        bottomCornerRadius: Float
    ) {
        // TODO(b/184121838): Add default support for GradientDrawable and LayerDrawable to make
        // this work out of the box for common rounded backgrounds.
        // By default, we rely on WrappedDrawable to set/restore the background radii before/after
        // each draw.
        backgroundDrawable?.setBackgroundRadius(topCornerRadius, bottomCornerRadius)
    }

    /** Return the current top corner radius of the background. */
    protected open fun getCurrentTopCornerRadius(): Float = 0f
    protected open fun getCurrentTopCornerRadius(): Float {
        val drawable = getBackground() ?: return 0f
        val gradient = findGradientDrawable(drawable) ?: return 0f

        // TODO(b/184121838): Support more than symmetric top & bottom radius.
        return gradient.cornerRadii?.get(CORNER_RADIUS_TOP_INDEX) ?: gradient.cornerRadius
    }

    /** Return the current bottom corner radius of the background. */
    protected open fun getCurrentBottomCornerRadius(): Float = 0f
    protected open fun getCurrentBottomCornerRadius(): Float {
        val drawable = getBackground() ?: return 0f
        val gradient = findGradientDrawable(drawable) ?: return 0f

        // TODO(b/184121838): Support more than symmetric top & bottom radius.
        return gradient.cornerRadii?.get(CORNER_RADIUS_BOTTOM_INDEX) ?: gradient.cornerRadius
    }

    override fun getRootView(): View {
        return rootView
@@ -94,7 +109,7 @@ open class GhostedViewLaunchAnimatorController(

    override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
        backgroundView = FrameLayout(rootView.context).apply {
            forceHasOverlappingRendering(true)
            forceHasOverlappingRendering(false)
        }
        rootViewOverlay.add(backgroundView)

@@ -143,6 +158,33 @@ open class GhostedViewLaunchAnimatorController(
        ghostedView.invalidate()
    }

    companion object {
        private const val CORNER_RADIUS_TOP_INDEX = 0
        private const val CORNER_RADIUS_BOTTOM_INDEX = 4

        /**
         * Return the first [GradientDrawable] found in [drawable], or null if none is found. If
         * [drawable] is a [LayerDrawable], this will return the first layer that is a
         * [GradientDrawable].
         */
        private fun findGradientDrawable(drawable: Drawable): GradientDrawable? {
            if (drawable is GradientDrawable) {
                return drawable
            }

            if (drawable is LayerDrawable) {
                for (i in 0 until drawable.numberOfLayers) {
                    val maybeGradient = drawable.getDrawable(i)
                    if (maybeGradient is GradientDrawable) {
                        return maybeGradient
                    }
                }
            }

            return null
        }
    }

    private class WrappedDrawable(val wrapped: Drawable?) : Drawable() {
        companion object {
            private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
@@ -151,6 +193,9 @@ open class GhostedViewLaunchAnimatorController(
        private var currentAlpha = 0xFF
        private var previousBounds = Rect()

        private var cornerRadii = FloatArray(8) { -1f }
        private var previousCornerRadii = FloatArray(8)

        override fun draw(canvas: Canvas) {
            val wrapped = this.wrapped ?: return

@@ -158,7 +203,8 @@ open class GhostedViewLaunchAnimatorController(

            wrapped.alpha = currentAlpha
            wrapped.bounds = bounds
            wrapped.setXfermode(SRC_MODE)
            setXfermode(wrapped, SRC_MODE)
            applyBackgroundRadii()

            wrapped.draw(canvas)

@@ -167,7 +213,8 @@ open class GhostedViewLaunchAnimatorController(
            // background.
            wrapped.alpha = 0
            wrapped.bounds = previousBounds
            wrapped.setXfermode(null)
            setXfermode(wrapped, null)
            restoreBackgroundRadii()
        }

        override fun setAlpha(alpha: Int) {
@@ -192,5 +239,91 @@ open class GhostedViewLaunchAnimatorController(
        override fun setColorFilter(filter: ColorFilter?) {
            wrapped?.colorFilter = filter
        }

        private fun setXfermode(background: Drawable, mode: PorterDuffXfermode?) {
            if (background !is LayerDrawable) {
                background.setXfermode(mode)
                return
            }

            // We set the xfermode on the first layer that is not a mask. Most of the time it will
            // be the "background layer".
            for (i in 0 until background.numberOfLayers) {
                if (background.getId(i) != android.R.id.mask) {
                    background.getDrawable(i).setXfermode(mode)
                    break
                }
            }
        }

        fun setBackgroundRadius(topCornerRadius: Float, bottomCornerRadius: Float) {
            updateRadii(cornerRadii, topCornerRadius, bottomCornerRadius)
            invalidateSelf()
        }

        private fun updateRadii(
            radii: FloatArray,
            topCornerRadius: Float,
            bottomCornerRadius: Float
        ) {
            radii[0] = topCornerRadius
            radii[1] = topCornerRadius
            radii[2] = topCornerRadius
            radii[3] = topCornerRadius

            radii[4] = bottomCornerRadius
            radii[5] = bottomCornerRadius
            radii[6] = bottomCornerRadius
            radii[7] = bottomCornerRadius
        }

        private fun applyBackgroundRadii() {
            if (cornerRadii[0] < 0 || wrapped == null) {
                return
            }

            savePreviousBackgroundRadii(wrapped)
            applyBackgroundRadii(wrapped, cornerRadii)
        }

        private fun savePreviousBackgroundRadii(background: Drawable) {
            // TODO(b/184121838): This method assumes that all GradientDrawable in background will
            // have the same radius. Should we save/restore the radii for each layer instead?
            val gradient = findGradientDrawable(background) ?: return

            // TODO(b/184121838): GradientDrawable#getCornerRadii clones its radii array. Should we
            // try to avoid that?
            val radii = gradient.cornerRadii
            if (radii != null) {
                radii.copyInto(previousCornerRadii)
            } else {
                // Copy the cornerRadius into previousCornerRadii.
                val radius = gradient.cornerRadius
                updateRadii(previousCornerRadii, radius, radius)
            }
        }

        private fun applyBackgroundRadii(drawable: Drawable, radii: FloatArray) {
            if (drawable is GradientDrawable) {
                drawable.cornerRadii = radii
                return
            }

            if (drawable !is LayerDrawable) {
                return
            }

            for (i in 0 until drawable.numberOfLayers) {
                (drawable.getDrawable(i) as? GradientDrawable)?.cornerRadii = radii
            }
        }

        private fun restoreBackgroundRadii() {
            if (cornerRadii[0] < 0 || wrapped == null) {
                return
            }

            applyBackgroundRadii(wrapped, previousCornerRadii)
        }
    }
}
+10 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ import android.app.PendingIntent;
import android.content.Intent;
import android.view.View;

import androidx.annotation.Nullable;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.animation.ActivityLaunchAnimator;
@@ -106,6 +108,14 @@ public class ActivityStarterDelegate implements ActivityStarter {
                starter -> starter.get().postStartActivityDismissingKeyguard(intent, delay));
    }

    @Override
    public void postStartActivityDismissingKeyguard(Intent intent, int delay,
            @Nullable ActivityLaunchAnimator.Controller animationController) {
        mActualStarter.ifPresent(
                starter -> starter.get().postStartActivityDismissingKeyguard(intent, delay,
                        animationController));
    }

    @Override
    public void postStartActivityDismissingKeyguard(PendingIntent intent) {
        mActualStarter.ifPresent(
+81 −50
Original line number Diff line number Diff line
@@ -1803,7 +1803,8 @@ public class StatusBar extends SystemUI implements DemoMode,
    @Override
    public void startActivity(Intent intent, boolean dismissShade, Callback callback) {
        startActivityDismissingKeyguard(intent, false, dismissShade,
                false /* disallowEnterPictureInPictureWhileLaunching */, callback, 0);
                false /* disallowEnterPictureInPictureWhileLaunching */, callback, 0,
                null /* animationController */);
    }

    public void setQsExpanded(boolean expanded) {
@@ -2026,7 +2027,7 @@ public class StatusBar extends SystemUI implements DemoMode,
    /** Whether we should animate an activity launch. */
    public boolean areLaunchAnimationsEnabled() {
        // TODO(b/184121838): Support lock screen launch animations.
        return mState == StatusBarState.SHADE;
        return mState == StatusBarState.SHADE && !isOccluded();
    }

    public boolean isDeviceInVrMode() {
@@ -2730,7 +2731,7 @@ public class StatusBar extends SystemUI implements DemoMode,
            boolean dismissShade, int flags) {
        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade,
                false /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */,
                flags);
                flags, null /* animationController */);
    }

    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
@@ -2738,21 +2739,37 @@ public class StatusBar extends SystemUI implements DemoMode,
        startActivityDismissingKeyguard(intent, onlyProvisioned, dismissShade, 0);
    }

    public void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
    private void startActivityDismissingKeyguard(final Intent intent, boolean onlyProvisioned,
            final boolean dismissShade, final boolean disallowEnterPictureInPictureWhileLaunching,
            final Callback callback, int flags) {
            final Callback callback, int flags,
            @Nullable ActivityLaunchAnimator.Controller animationController) {
        if (onlyProvisioned && !mDeviceProvisionedController.isDeviceProvisioned()) return;

        final boolean afterKeyguardGone = mActivityIntentHelper.wouldLaunchResolverActivity(
                intent, mLockscreenUserManager.getCurrentUserId());

        ActivityLaunchAnimator.Controller animController = null;
        if (animationController != null && areLaunchAnimationsEnabled()) {
            animController = dismissShade ? new StatusBarLaunchAnimatorController(
                    animationController, this, true /* isLaunchForActivity */)
                    : animationController;
        }
        final ActivityLaunchAnimator.Controller animCallbackForLambda = animController;

        // If we animate, we will dismiss the shade only once the animation is done. This is taken
        // care of by the StatusBarLaunchAnimationController.
        boolean dismissShadeDirectly = dismissShade && animController == null;

        Runnable runnable = () -> {
            mAssistManagerLazy.get().hideAssist();
            intent.setFlags(
                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.addFlags(flags);
            int result = ActivityManager.START_CANCELED;
            ActivityOptions options = new ActivityOptions(getActivityOptions(mDisplayId,
                    null /* remoteAnimation */));
            int[] result = new int[] { ActivityManager.START_CANCELED };

            mActivityLaunchAnimator.startIntentWithAnimation(animCallbackForLambda, (adapter) -> {
                ActivityOptions options = new ActivityOptions(
                        getActivityOptions(mDisplayId, adapter));
                options.setDisallowEnterPictureInPictureWhileLaunching(
                        disallowEnterPictureInPictureWhileLaunching);
                if (CameraIntents.isInsecureCameraIntent(intent)) {
@@ -2775,8 +2792,9 @@ public class StatusBar extends SystemUI implements DemoMode,
                    // if it is volume panel.
                    options.setDisallowEnterPictureInPictureWhileLaunching(true);
                }

                try {
                result = ActivityTaskManager.getService().startActivityAsUser(
                    result[0] = ActivityTaskManager.getService().startActivityAsUser(
                            null, mContext.getBasePackageName(), mContext.getAttributionTag(),
                            intent,
                            intent.resolveTypeIfNeeded(mContext.getContentResolver()),
@@ -2785,8 +2803,11 @@ public class StatusBar extends SystemUI implements DemoMode,
                } catch (RemoteException e) {
                    Log.w(TAG, "Unable to start activity", e);
                }
                return result[0];
            });

            if (callback != null) {
                callback.onActivityStarted(result);
                callback.onActivityStarted(result[0]);
            }
        };
        Runnable cancelRunnable = () -> {
@@ -2794,7 +2815,7 @@ public class StatusBar extends SystemUI implements DemoMode,
                callback.onActivityStarted(ActivityManager.START_CANCELED);
            }
        };
        executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShade,
        executeRunnableDismissingKeyguard(runnable, cancelRunnable, dismissShadeDirectly,
                afterKeyguardGone, true /* deferred */);
    }

@@ -3152,12 +3173,21 @@ public class StatusBar extends SystemUI implements DemoMode,

    @Override
    public void postStartActivityDismissingKeyguard(final Intent intent, int delay) {
        mHandler.postDelayed(() ->
                handleStartActivityDismissingKeyguard(intent, true /*onlyProvisioned*/), delay);
        postStartActivityDismissingKeyguard(intent, delay, null /* animationController */);
    }

    private void handleStartActivityDismissingKeyguard(Intent intent, boolean onlyProvisioned) {
        startActivityDismissingKeyguard(intent, onlyProvisioned, true /* dismissShade */);
    @Override
    public void postStartActivityDismissingKeyguard(Intent intent, int delay,
            @Nullable ActivityLaunchAnimator.Controller animationController) {
        mHandler.postDelayed(
                () ->
                        startActivityDismissingKeyguard(intent, true /* onlyProvisioned */,
                                true /* dismissShade */,
                                false /* disallowEnterPictureInPictureWhileLaunching */,
                                null /* callback */,
                                0 /* flags */,
                                animationController),
                delay);
    }

    @Override
@@ -4072,7 +4102,8 @@ public class StatusBar extends SystemUI implements DemoMode,
            final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext);
            startActivityDismissingKeyguard(cameraIntent,
                    false /* onlyProvisioned */, true /* dismissShade */,
                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0);
                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
                    null /* animationController */);
        } else {
            if (!mDeviceInteractive) {
                // Avoid flickering of the scrim when we instant launch the camera and the bouncer
@@ -4123,7 +4154,8 @@ public class StatusBar extends SystemUI implements DemoMode,
        if (!mStatusBarKeyguardViewManager.isShowing()) {
            startActivityDismissingKeyguard(emergencyIntent,
                    false /* onlyProvisioned */, true /* dismissShade */,
                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0);
                    true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0,
                    null /* animationController */);
            return;
        }

@@ -4505,8 +4537,7 @@ public class StatusBar extends SystemUI implements DemoMode,
                && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
                mLockscreenUserManager.getCurrentUserId());

        boolean animate =
                animationController != null && areLaunchAnimationsEnabled() && !isOccluded();
        boolean animate = animationController != null && areLaunchAnimationsEnabled();
        boolean collapse = !animate;
        executeActionDismissingKeyguard(() -> {
            try {