From 7d830825884fe258d0a92a14592703f83f0d7b63 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Thu, 1 Apr 2021 11:32:49 +0200 Subject: [PATCH 1/3] Animate the media activity launch. This CL animates the activity launched when clicking the media player in the shade. See b/174236650#comment3 for before/after videos. Bug: 174236650 Test: Tap the media player when the shade is open and unlocked. Change-Id: I147d627f07023e0d06d25a7ebec42d14cbbc314a --- .../systemui/media/IlluminationDrawable.kt | 49 ++++++++++++++-- .../systemui/media/LightSourceDrawable.kt | 9 ++- .../systemui/media/MediaControlPanel.java | 56 +++++++++++++++++-- 3 files changed, 101 insertions(+), 13 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt index 10b36e9eb85a0..bc451137d7f53 100644 --- a/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/IlluminationDrawable.kt @@ -28,6 +28,7 @@ import android.graphics.ColorFilter import android.graphics.Outline import android.graphics.Paint import android.graphics.PixelFormat +import android.graphics.Xfermode import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.MathUtils @@ -48,7 +49,15 @@ private const val BACKGROUND_ANIM_DURATION = 370L class IlluminationDrawable : Drawable() { private var themeAttrs: IntArray? = null - private var cornerRadius = 0f + private var cornerRadiusOverride = -1f + var cornerRadius = 0f + get() { + return if (cornerRadiusOverride >= 0) { + cornerRadiusOverride + } else { + field + } + } private var highlightColor = Color.TRANSPARENT private var tmpHsl = floatArrayOf(0f, 0f, 0f) private var paint = Paint() @@ -122,8 +131,28 @@ class IlluminationDrawable : Drawable() { throw UnsupportedOperationException("Color filters are not supported") } - override fun setAlpha(value: Int) { - throw UnsupportedOperationException("Alpha is not supported") + override fun setAlpha(alpha: Int) { + if (alpha == paint.alpha) { + return + } + + paint.alpha = alpha + invalidateSelf() + + lightSources.forEach { it.alpha = alpha } + } + + override fun getAlpha(): Int { + return paint.alpha + } + + override fun setXfermode(mode: Xfermode?) { + if (mode == paint.xfermode) { + return + } + + paint.xfermode = mode + invalidateSelf() } /** @@ -171,9 +200,19 @@ class IlluminationDrawable : Drawable() { fun registerLightSource(lightSource: View) { if (lightSource.background is LightSourceDrawable) { - lightSources.add(lightSource.background as LightSourceDrawable) + registerLightSource(lightSource.background as LightSourceDrawable) } else if (lightSource.foreground is LightSourceDrawable) { - lightSources.add(lightSource.foreground as LightSourceDrawable) + registerLightSource(lightSource.foreground as LightSourceDrawable) } } + + private fun registerLightSource(lightSource: LightSourceDrawable) { + lightSource.alpha = paint.alpha + lightSources.add(lightSource) + } + + /** Set or remove the corner radius override. This is typically set during animations. */ + fun setCornerRadiusOverride(cornerRadius: Float?) { + cornerRadiusOverride = cornerRadius ?: -1f + } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt b/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt index cee71013413d5..e6035f35fe717 100644 --- a/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/media/LightSourceDrawable.kt @@ -184,8 +184,13 @@ class LightSourceDrawable : Drawable() { throw UnsupportedOperationException("Color filters are not supported") } - override fun setAlpha(value: Int) { - throw UnsupportedOperationException("Alpha is not supported") + override fun setAlpha(alpha: Int) { + if (alpha == paint.alpha) { + return + } + + paint.alpha = alpha + invalidateSelf() } /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index d3ae9320d91d8..4fd8fe7b921be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -49,6 +49,8 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.animation.ActivityLaunchAnimator; +import com.android.systemui.plugins.animation.GhostedViewLaunchAnimatorController; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.util.animation.TransitionLayout; @@ -101,11 +103,12 @@ public class MediaControlPanel { // This will provide the corners for the album art. private final ViewOutlineProvider mViewOutlineProvider; private final MediaOutputDialogFactory mMediaOutputDialogFactory; + /** * Initialize a new control panel - * @param context + * * @param backgroundExecutor background executor, used for processing artwork - * @param activityStarter activity starter + * @param activityStarter activity starter */ @Inject public MediaControlPanel(Context context, @Background Executor backgroundExecutor, @@ -147,6 +150,7 @@ public class MediaControlPanel { /** * Get the view holder used to display media controls + * * @return the view holder */ @Nullable @@ -156,6 +160,7 @@ public class MediaControlPanel { /** * Get the view controller used to display media controls + * * @return the media view controller */ @NonNull @@ -165,7 +170,7 @@ public class MediaControlPanel { /** * Sets the listening state of the player. - * + *

* Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid * unnecessary work when the QS panel is closed. * @@ -177,6 +182,7 @@ public class MediaControlPanel { /** * Get the context + * * @return context */ public Context getContext() { @@ -244,7 +250,8 @@ public class MediaControlPanel { if (clickIntent != null) { mViewHolder.getPlayer().setOnClickListener(v -> { if (mMediaViewController.isGutsVisible()) return; - mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); + mActivityStarter.postStartActivityDismissingKeyguard(clickIntent, + buildLaunchAnimatorController(mViewHolder.getPlayer())); }); } @@ -396,8 +403,42 @@ public class MediaControlPanel { mMediaViewController.refreshState(); } + @Nullable + private ActivityLaunchAnimator.Controller buildLaunchAnimatorController( + TransitionLayout player) { + // TODO(b/174236650): Make sure that the carousel indicator also fades out. + // TODO(b/174236650): Instrument the animation to measure jank. + return new GhostedViewLaunchAnimatorController(player) { + @Override + protected float getCurrentTopCornerRadius() { + return ((IlluminationDrawable) player.getBackground()).getCornerRadius(); + } + + @Override + protected float getCurrentBottomCornerRadius() { + // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius. + return getCurrentTopCornerRadius(); + } + + @Override + protected void setBackgroundCornerRadius(Drawable background, float topCornerRadius, + float bottomCornerRadius) { + // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius. + float radius = Math.min(topCornerRadius, bottomCornerRadius); + ((IlluminationDrawable) background).setCornerRadiusOverride(radius); + } + + @Override + public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) { + super.onLaunchAnimationEnd(isExpandingFullyAbove); + ((IlluminationDrawable) player.getBackground()).setCornerRadiusOverride(null); + } + }; + } + /** * Close the guts for this player. + * * @param immediate {@code true} if it should be closed without animation */ public void closeGuts(boolean immediate) { @@ -427,7 +468,7 @@ public class MediaControlPanel { if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; - bounds.offset((int) -offsetX,(int) -offsetY); + bounds.offset((int) -offsetX, (int) -offsetY); } drawable.setBounds(bounds); return drawable; @@ -435,6 +476,7 @@ public class MediaControlPanel { /** * Get the current media controller + * * @return the controller */ public MediaController getController() { @@ -443,6 +485,7 @@ public class MediaControlPanel { /** * Check whether the media controlled by this player is currently playing + * * @return whether it is playing, or false if no controller information */ public boolean isPlaying() { @@ -451,6 +494,7 @@ public class MediaControlPanel { /** * Check whether the given controller is currently playing + * * @param controller media controller to check * @return whether it is playing, or false if no controller information */ @@ -468,7 +512,7 @@ public class MediaControlPanel { } private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { - set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE); + set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : ConstraintSet.GONE); set.setAlpha(actionId, visible ? 1.0f : 0.0f); } } From b796cfec0ab1521296c2361c68d0684ef6556212 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 31 Mar 2021 16:33:07 +0200 Subject: [PATCH 2/3] 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 --- .../systemui/plugins/ActivityStarter.java | 2 + .../animation/ActivityLaunchAnimator.kt | 4 +- .../GhostedViewLaunchAnimatorController.kt | 147 +++++++++++++++++- .../systemui/ActivityStarterDelegate.java | 10 ++ .../systemui/statusbar/phone/StatusBar.java | 131 ++++++++++------ 5 files changed, 235 insertions(+), 59 deletions(-) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index e003b2e1d68ac..055fe373156ad 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -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); /** diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt index e8bdb6755f97d..5af8dab764a12 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/ActivityLaunchAnimator.kt @@ -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) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt index a23722433a4db..a5494adedd1db 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/animation/GhostedViewLaunchAnimatorController.kt @@ -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) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java index ecbb70eb0f039..5507ffabc3a0c 100644 --- a/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/ActivityStarterDelegate.java @@ -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( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 5fd0d6602a948..54256f0cff4fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -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,55 +2739,75 @@ 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 */)); - options.setDisallowEnterPictureInPictureWhileLaunching( - disallowEnterPictureInPictureWhileLaunching); - if (CameraIntents.isInsecureCameraIntent(intent)) { - // Normally an activity will set it's requested rotation - // animation on its window. However when launching an activity - // causes the orientation to change this is too late. In these cases - // the default animation is used. This doesn't look good for - // the camera (as it rotates the camera contents out of sync - // with physical reality). So, we ask the WindowManager to - // force the crossfade animation if an orientation change - // happens to occur during the launch. - options.setRotationAnimationHint( - WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); - } - if (intent.getAction() == Settings.Panel.ACTION_VOLUME) { - // Settings Panel is implemented as activity(not a dialog), so - // underlying app is paused and may enter picture-in-picture mode - // as a result. - // So we need to disable picture-in-picture mode here - // if it is volume panel. - options.setDisallowEnterPictureInPictureWhileLaunching(true); - } - try { - result = ActivityTaskManager.getService().startActivityAsUser( - null, mContext.getBasePackageName(), mContext.getAttributionTag(), - intent, - intent.resolveTypeIfNeeded(mContext.getContentResolver()), - null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, - options.toBundle(), UserHandle.CURRENT.getIdentifier()); - } catch (RemoteException e) { - Log.w(TAG, "Unable to start activity", e); - } + 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)) { + // Normally an activity will set it's requested rotation + // animation on its window. However when launching an activity + // causes the orientation to change this is too late. In these cases + // the default animation is used. This doesn't look good for + // the camera (as it rotates the camera contents out of sync + // with physical reality). So, we ask the WindowManager to + // force the crossfade animation if an orientation change + // happens to occur during the launch. + options.setRotationAnimationHint( + WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS); + } + if (intent.getAction() == Settings.Panel.ACTION_VOLUME) { + // Settings Panel is implemented as activity(not a dialog), so + // underlying app is paused and may enter picture-in-picture mode + // as a result. + // So we need to disable picture-in-picture mode here + // if it is volume panel. + options.setDisallowEnterPictureInPictureWhileLaunching(true); + } + + try { + result[0] = ActivityTaskManager.getService().startActivityAsUser( + null, mContext.getBasePackageName(), mContext.getAttributionTag(), + intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), + null, null, 0, Intent.FLAG_ACTIVITY_NEW_TASK, null, + options.toBundle(), UserHandle.CURRENT.getIdentifier()); + } 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 { From 49bcc0d1ee42ec93ec361c1046b14f12066c2cba Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 31 Mar 2021 16:46:18 +0200 Subject: [PATCH 3/3] Animate QuickSettings long press (1/2). See b/172063474#comment3 for before/after videos. Bug: 172063474 Test: Long press any QS tile Change-Id: I75e4784bd6db92a1aea6ffd0eabb648e82376a09 --- .../android/systemui/plugins/qs/QSTile.java | 12 +++++++++-- .../systemui/qs/tileimpl/QSTileBaseView.java | 2 +- .../systemui/qs/tileimpl/QSTileImpl.java | 21 +++++++++++++------ .../android/systemui/qs/tiles/CastTile.java | 2 +- .../systemui/qs/tiles/FlashlightTile.java | 3 ++- .../qs/customize/TileQueryHelperTest.java | 4 +++- .../systemui/qs/tileimpl/QSTileImplTest.java | 4 ++-- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java index c9f2401fd16a9..4623714891589 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java @@ -15,10 +15,12 @@ package com.android.systemui.plugins.qs; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.service.quicksettings.Tile; +import android.view.View; import com.android.internal.logging.InstanceId; import com.android.systemui.plugins.annotations.DependsOn; @@ -53,10 +55,16 @@ public interface QSTile { void removeCallbacks(); QSIconView createTileView(Context context); - + void click(); void secondaryClick(); - void longClick(); + + /** + * The tile was long clicked. + * + * @param view The view that was clicked. + */ + void longClick(@Nullable View view); void userSwitch(int currentUser); int getMetricsCategory(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java index a45b131902c85..abe321979a1a0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java @@ -173,7 +173,7 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { @Override public void init(QSTile tile) { init(v -> tile.click(), v -> tile.secondaryClick(), view -> { - tile.longClick(); + tile.longClick(this); return true; }); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java index a17aebad19896..f5c38136e35c6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java @@ -31,6 +31,7 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.annotation.CallSuper; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -43,6 +44,7 @@ import android.text.format.DateUtils; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; +import android.view.View; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; @@ -58,6 +60,7 @@ import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.animation.ActivityLaunchAnimator; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSIconView; import com.android.systemui.plugins.qs.QSTile; @@ -290,14 +293,15 @@ public abstract class QSTileImpl implements QSTile, Lifecy mHandler.sendEmptyMessage(H.SECONDARY_CLICK); } - public void longClick() { + @Override + public void longClick(@Nullable View view) { mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION) .addTaggedData(FIELD_STATUS_BAR_STATE, mStatusBarStateController.getState()))); mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(), getInstanceId()); mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state); - mHandler.sendEmptyMessage(H.LONG_CLICK); + mHandler.obtainMessage(H.LONG_CLICK, view).sendToTarget(); } public LogMaker populate(LogMaker logMaker) { @@ -372,10 +376,15 @@ public abstract class QSTileImpl implements QSTile, Lifecy /** * Handles long click on the tile by launching the {@link Intent} defined in - * {@link QSTileImpl#getLongClickIntent} + * {@link QSTileImpl#getLongClickIntent}. + * + * @param view The view from which the opening window will be animated. */ - protected void handleLongClick() { - mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0); + protected void handleLongClick(@Nullable View view) { + ActivityLaunchAnimator.Controller animationController = + view != null ? ActivityLaunchAnimator.Controller.fromView(view) : null; + mActivityStarter.postStartActivityDismissingKeyguard(getLongClickIntent(), 0, + animationController); } /** @@ -614,7 +623,7 @@ public abstract class QSTileImpl implements QSTile, Lifecy handleSecondaryClick(); } else if (msg.what == LONG_CLICK) { name = "handleLongClick"; - handleLongClick(); + handleLongClick((View) msg.obj); } else if (msg.what == REFRESH_STATE) { name = "handleRefreshState"; handleRefreshState(msg.obj); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java index fa99eed150e3f..d78dbae9d439b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java @@ -142,7 +142,7 @@ public class CastTile extends QSTileImpl { } @Override - protected void handleLongClick() { + protected void handleLongClick(View view) { handleClick(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 31a98db033f90..b7cb615e166f7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.service.quicksettings.Tile; +import android.view.View; import android.widget.Switch; import com.android.internal.logging.MetricsLogger; @@ -107,7 +108,7 @@ public class FlashlightTile extends QSTileImpl implements } @Override - protected void handleLongClick() { + protected void handleLongClick(View view) { handleClick(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java index f2f9656b23d07..c35f8b6f0b0dc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.Manifest; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -46,6 +47,7 @@ import android.testing.TestableLooper; import android.text.TextUtils; import android.util.ArraySet; import android.util.FeatureFlagUtils; +import android.view.View; import androidx.test.filters.SmallTest; @@ -418,7 +420,7 @@ public class TileQueryHelperTest extends SysuiTestCase { public void secondaryClick() {} @Override - public void longClick() {} + public void longClick(@Nullable View view) {} @Override public void userSwitch(int currentUser) {} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java index 937ab1c5c41d8..0f9ca7b57328b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java @@ -189,7 +189,7 @@ public class QSTileImplTest extends SysuiTestCase { @Test public void testLongClick_Metrics() { - mTile.longClick(); + mTile.longClick(null /* view */); verify(mMetricsLogger).write(argThat(new TileLogMatcher(ACTION_QS_LONG_PRESS))); assertEquals(1, mUiEventLoggerFake.numLogs()); UiEventLoggerFake.FakeUiEvent event = mUiEventLoggerFake.get(0); @@ -201,7 +201,7 @@ public class QSTileImplTest extends SysuiTestCase { public void testLongClick_log() { when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); - mTile.longClick(); + mTile.longClick(null /* view */); verify(mQsLogger).logTileLongClick(SPEC, StatusBarState.SHADE, Tile.STATE_ACTIVE); }