diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index d75474d5279d3..530db0d0304a4 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -53,6 +53,15 @@ app:layout_constraintTop_toTopOf="@id/album_art" app:layout_constraintBottom_toBottomOf="@id/album_art" /> + + + + + + + + (R.id.album_art) val multiRippleView = itemView.requireViewById(R.id.touch_ripple_view) + val turbulenceNoiseView = + itemView.requireViewById(R.id.turbulence_noise_view) val appIcon = itemView.requireViewById(R.id.icon) val titleText = itemView.requireViewById(R.id.header_title) val artistText = itemView.requireViewById(R.id.header_artist) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt index bbf4ef17dea67..93be6a78ccd5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/ColorSchemeTransition.kt @@ -30,6 +30,7 @@ import com.android.settingslib.Utils import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController /** * A [ColorTransition] is an object that updates the colors of views each time [updateColorScheme] @@ -102,13 +103,21 @@ internal constructor( private val context: Context, private val mediaViewHolder: MediaViewHolder, private val multiRippleController: MultiRippleController, + private val turbulenceNoiseController: TurbulenceNoiseController, animatingColorTransitionFactory: AnimatingColorTransitionFactory ) { constructor( context: Context, mediaViewHolder: MediaViewHolder, multiRippleController: MultiRippleController, - ) : this(context, mediaViewHolder, multiRippleController, ::AnimatingColorTransition) + turbulenceNoiseController: TurbulenceNoiseController + ) : this( + context, + mediaViewHolder, + multiRippleController, + turbulenceNoiseController, + ::AnimatingColorTransition + ) val bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95) val surfaceColor = @@ -129,6 +138,7 @@ internal constructor( mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) multiRippleController.updateColor(accentPrimary) + turbulenceNoiseController.updateNoiseColor(accentPrimary) } val accentSecondary = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index 4eb0d991bd4c0..cb5789e20c15d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.ColorStateList; +import android.graphics.BlendMode; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -64,6 +65,7 @@ import androidx.annotation.UiThread; import androidx.constraintlayout.widget.ConstraintSet; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; import com.android.settingslib.widget.AdaptiveIcon; @@ -101,9 +103,12 @@ import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.surfaceeffects.ripple.MultiRippleController; +import com.android.systemui.surfaceeffects.ripple.MultiRippleView; import com.android.systemui.surfaceeffects.ripple.RippleAnimation; import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig; import com.android.systemui.surfaceeffects.ripple.RippleShader; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig; +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController; import com.android.systemui.util.ColorUtilKt; import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.time.SystemClock; @@ -216,7 +221,9 @@ public class MediaControlPanel { private boolean mShowBroadcastDialogButton = false; private String mSwitchBroadcastApp; private MultiRippleController mMultiRippleController; + private TurbulenceNoiseController mTurbulenceNoiseController; private FeatureFlags mFeatureFlags; + private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null; /** * Initialize a new control panel @@ -393,9 +400,20 @@ public class MediaControlPanel { AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); - mMultiRippleController = new MultiRippleController(vh.getMultiRippleView()); + MultiRippleView multiRippleView = vh.getMultiRippleView(); + mMultiRippleController = new MultiRippleController(multiRippleView); + mTurbulenceNoiseController = new TurbulenceNoiseController(vh.getTurbulenceNoiseView()); + multiRippleView.addRipplesFinishedListener( + () -> { + if (mTurbulenceNoiseAnimationConfig == null) { + mTurbulenceNoiseAnimationConfig = createLingeringNoiseAnimation(); + } + // Color will be correctly updated in ColorSchemeTransition. + mTurbulenceNoiseController.play(mTurbulenceNoiseAnimationConfig); + } + ); mColorSchemeTransition = new ColorSchemeTransition( - mContext, mMediaViewHolder, mMultiRippleController); + mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController); mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter); } @@ -1025,7 +1043,7 @@ public class MediaControlPanel { /* maxWidth= */ maxSize, /* maxHeight= */ maxSize, /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density, - mColorSchemeTransition.getAccentPrimary().getTargetColor(), + mColorSchemeTransition.getAccentPrimary().getCurrentColor(), /* opacity= */ 100, /* shouldFillRipple= */ false, /* sparkleStrength= */ 0f, @@ -1034,6 +1052,26 @@ public class MediaControlPanel { ); } + private TurbulenceNoiseAnimationConfig createLingeringNoiseAnimation() { + return new TurbulenceNoiseAnimationConfig( + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_GRID_COUNT, + TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER, + /* noiseMoveSpeedX= */ 0f, + /* noiseMoveSpeedY= */ 0f, + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, + /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), + // We want to add (BlendMode.PLUS) the turbulence noise on top of the album art. + // Thus, set the background color with alpha 0. + /* backgroundColor= */ ColorUtils.setAlphaComponent(Color.BLACK, 0), + TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY, + /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(), + /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(), + TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_DURATION_IN_MILLIS, + this.getContext().getResources().getDisplayMetrics().density, + BlendMode.PLUS, + /* onAnimationEnd= */ null + ); + } private void clearButton(final ImageButton button) { button.setImageDrawable(null); button.setContentDescription(null); diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt index e44daf4d7cc20..f558fee776e6a 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt @@ -31,11 +31,21 @@ import android.view.View class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { internal val ripples = ArrayList() + private val listeners = ArrayList() private val ripplePaint = Paint() private var isWarningLogged = false companion object { private const val TAG = "MultiRippleView" + + interface RipplesFinishedListener { + /** Triggered when all the ripples finish running. */ + fun onRipplesFinish() + } + } + + fun addRipplesFinishedListener(listener: RipplesFinishedListener) { + listeners.add(listener) } override fun onDraw(canvas: Canvas?) { @@ -62,6 +72,10 @@ class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, a shouldInvalidate = shouldInvalidate || anim.isPlaying() } - if (shouldInvalidate) invalidate() + if (shouldInvalidate) { + invalidate() + } else { // Nothing is playing. + listeners.forEach { listener -> listener.onRipplesFinish() } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt index a4540686640ad..5ac3aad749fc1 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -43,7 +43,7 @@ data class TurbulenceNoiseAnimationConfig( val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z, /** Color of the effect. */ - val color: Int = DEFAULT_COLOR, + var color: Int = DEFAULT_COLOR, /** Background color of the effect. */ val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR, val opacity: Int = DEFAULT_OPACITY, diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt index d900df6417560..4c7e5f4c70937 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt @@ -17,6 +17,11 @@ package com.android.systemui.surfaceeffects.turbulencenoise /** A controller that plays [TurbulenceNoiseView]. */ class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) { + /** Updates the color of the noise. */ + fun updateNoiseColor(color: Int) { + turbulenceNoiseView.updateColor(color) + } + // TODO: add cancel and/ or pause once design requirements become clear. /** Plays [TurbulenceNoiseView] with the given config. */ fun play(turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig) { diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt index 5bbebcd6c799e..19c114d2693c6 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -104,8 +104,8 @@ class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) { * * Expected value range is [1, 0]. */ - fun setOpacity(fadeOut: Float) { - setFloatUniform("in_opacity", fadeOut) + fun setOpacity(opacity: Float) { + setFloatUniform("in_opacity", opacity) } /** Sets the size of the shader. */ diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt index 2d71596b2a3c5..8649d59245874 100644 --- a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt +++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt @@ -40,6 +40,7 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex private val paint = Paint().apply { this.shader = turbulenceNoiseShader } private val random = Random() private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + private var config: TurbulenceNoiseAnimationConfig? = null val isPlaying: Boolean get() = animator.isRunning @@ -49,6 +50,14 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex visibility = INVISIBLE } + /** Updates the color during the animation. No-op if there's no animation playing. */ + fun updateColor(color: Int) { + config?.let { + it.color = color + applyConfig(it) + } + } + override fun onDraw(canvas: Canvas?) { if (canvas == null || !canvas.isHardwareAccelerated) { // Drawing with the turbulence noise shader requires hardware acceleration, so skip @@ -101,6 +110,7 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex } private fun applyConfig(config: TurbulenceNoiseAnimationConfig) { + this.config = config with(turbulenceNoiseShader) { setGridCount(config.gridCount) setColor(ColorUtils.setAlphaComponent(config.color, config.opacity)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt index 9a1e6f381169b..a94374680b915 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/ColorSchemeTransitionTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.monet.ColorScheme import com.android.systemui.surfaceeffects.ripple.MultiRippleController +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController import junit.framework.Assert.assertEquals import org.junit.After import org.junit.Before @@ -62,6 +63,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { @Mock private lateinit var mediaViewHolder: MediaViewHolder @Mock private lateinit var gutsViewHolder: GutsViewHolder @Mock private lateinit var multiRippleController: MultiRippleController + @Mock private lateinit var turbulenceNoiseController: TurbulenceNoiseController @JvmField @Rule val mockitoRule = MockitoJUnit.rule() @@ -76,6 +78,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() { context, mediaViewHolder, multiRippleController, + turbulenceNoiseController, animatingColorTransitionFactory ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index dad2524837d0f..c15a2cb83d966 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -81,6 +81,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.surfaceeffects.ripple.MultiRippleView +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView import com.android.systemui.util.animation.TransitionLayout import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.KotlinArgumentCaptor @@ -178,6 +179,7 @@ public class MediaControlPanelTest : SysuiTestCase() { private lateinit var dismiss: FrameLayout private lateinit var dismissText: TextView private lateinit var multiRippleView: MultiRippleView + private lateinit var turbulenceNoiseView: TurbulenceNoiseView private lateinit var session: MediaSession private lateinit var device: MediaDeviceData @@ -382,6 +384,7 @@ public class MediaControlPanelTest : SysuiTestCase() { } multiRippleView = MultiRippleView(context, null) + turbulenceNoiseView = TurbulenceNoiseView(context, null) whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.appIcon).thenReturn(appIcon) @@ -425,6 +428,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) + whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView) } /** Initialize elements for the recommendation view holder */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt new file mode 100644 index 0000000000000..2024d53b0212f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleViewTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.systemui.surfaceeffects.ripple + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MultiRippleViewTest : SysuiTestCase() { + private val fakeSystemClock = FakeSystemClock() + // FakeExecutor is needed to run animator. + private val fakeExecutor = FakeExecutor(fakeSystemClock) + + @Test + fun onRippleFinishes_triggersRippleFinished() { + val multiRippleView = MultiRippleView(context, null) + val multiRippleController = MultiRippleController(multiRippleView) + val rippleAnimationConfig = RippleAnimationConfig(duration = 1000L) + + var isTriggered = false + val listener = + object : MultiRippleView.Companion.RipplesFinishedListener { + override fun onRipplesFinish() { + isTriggered = true + } + } + multiRippleView.addRipplesFinishedListener(listener) + + fakeExecutor.execute { + val rippleAnimation = RippleAnimation(rippleAnimationConfig) + multiRippleController.play(rippleAnimation) + + fakeSystemClock.advanceTime(rippleAnimationConfig.duration) + + assertThat(isTriggered).isTrue() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt index 00c3bb9c15cf6..d25c8c1a58991 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.surfaceeffects.turbulencenoise +import android.graphics.Color import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -48,4 +49,23 @@ class TurbulenceNoiseControllerTest : SysuiTestCase() { assertThat(turbulenceNoiseView.isPlaying).isFalse() } } + + @Test + fun updateColor_updatesCorrectColor() { + val config = TurbulenceNoiseAnimationConfig(duration = 1000f, color = Color.WHITE) + val turbulenceNoiseView = TurbulenceNoiseView(context, null) + val expectedColor = Color.RED + + val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView) + + fakeExecutor.execute { + turbulenceNoiseController.play(config) + + turbulenceNoiseView.updateColor(expectedColor) + + fakeSystemClock.advanceTime(config.duration.toLong()) + + assertThat(config.color).isEqualTo(expectedColor) + } + } }