Add turbulence noise effects after ripple in UMO.

There will be a follow up CL to polish the turbulence noise.
This is a first pass.
Please find the recording in the bug.

Bug: 249536060
Test: TurbulenceNoiseControllerTest, MultiRippleControllerTest, MediaControlPanelTest, ColorSchemeTransitionTest

Change-Id: Iab2e9725e2b9fc6ec2eaa9f7721054458731fd0b
This commit is contained in:
Yein Jo
2022-11-07 09:52:38 +00:00
parent 4bfda86812
commit 480643fbf0
15 changed files with 202 additions and 8 deletions

View File

@@ -53,6 +53,15 @@
app:layout_constraintTop_toTopOf="@id/album_art"
app:layout_constraintBottom_toBottomOf="@id/album_art" />
<com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
android:id="@+id/turbulence_noise_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_expanded"
app:layout_constraintStart_toStartOf="@id/album_art"
app:layout_constraintEnd_toEndOf="@id/album_art"
app:layout_constraintTop_toTopOf="@id/album_art"
app:layout_constraintBottom_toBottomOf="@id/album_art" />
<!-- Guideline for output switcher -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/center_vertical_guideline"

View File

@@ -44,6 +44,16 @@
app:layout_constraintTop_toTopOf="@+id/album_art"
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
<!-- Turbulence noise must have the same constraint as the album art. -->
<Constraint
android:id="@+id/turbulence_noise_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_collapsed"
app:layout_constraintStart_toStartOf="@+id/album_art"
app:layout_constraintEnd_toEndOf="@+id/album_art"
app:layout_constraintTop_toTopOf="@+id/album_art"
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
<Constraint
android:id="@+id/header_title"
android:layout_width="wrap_content"

View File

@@ -37,6 +37,16 @@
app:layout_constraintTop_toTopOf="@+id/album_art"
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
<!-- Turbulence noise must have the same constraint as the album art. -->
<Constraint
android:id="@+id/turbulence_noise_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_media_session_height_expanded"
app:layout_constraintStart_toStartOf="@+id/album_art"
app:layout_constraintEnd_toEndOf="@+id/album_art"
app:layout_constraintTop_toTopOf="@+id/album_art"
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
<Constraint
android:id="@+id/header_title"
android:layout_width="wrap_content"

View File

@@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.Barrier
import com.android.systemui.R
import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.surfaceeffects.ripple.MultiRippleView
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
import com.android.systemui.util.animation.TransitionLayout
private const val TAG = "MediaViewHolder"
@@ -38,6 +39,8 @@ class MediaViewHolder constructor(itemView: View) {
// Player information
val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view)
val turbulenceNoiseView =
itemView.requireViewById<TurbulenceNoiseView>(R.id.turbulence_noise_view)
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)

View File

@@ -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 =

View File

@@ -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);

View File

@@ -31,11 +31,21 @@ import android.view.View
class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
internal val ripples = ArrayList<RippleAnimation>()
private val listeners = ArrayList<RipplesFinishedListener>()
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() }
}
}
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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. */

View File

@@ -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))

View File

@@ -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
)

View File

@@ -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 */

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
}