diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml
index 9b8b611558fec..530db0d0304a4 100644
--- a/packages/SystemUI/res/layout/media_session_view.xml
+++ b/packages/SystemUI/res/layout/media_session_view.xml
@@ -44,7 +44,7 @@
android:background="@drawable/qs_media_outline_album_bg"
/>
-
+
+
-
diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml
index 148e5ec1606fe..1eb621e0368b8 100644
--- a/packages/SystemUI/res/xml/media_session_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_session_collapsed.xml
@@ -44,6 +44,16 @@
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 918417fcd9a90..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
@@ -29,7 +29,8 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils
import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.monet.ColorScheme
-import com.android.systemui.ripple.MultiRippleController
+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 215fa03c8c596..47470525240c0 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;
@@ -97,13 +99,16 @@ import com.android.systemui.monet.ColorScheme;
import com.android.systemui.monet.Style;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.ripple.MultiRippleController;
-import com.android.systemui.ripple.RippleAnimation;
-import com.android.systemui.ripple.RippleAnimationConfig;
-import com.android.systemui.ripple.RippleShader;
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
@@ -394,9 +401,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);
}
@@ -1027,7 +1045,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,
@@ -1036,6 +1054,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/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index e354a03f17250..1ea202582f83d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -18,8 +18,8 @@ package com.android.systemui.media.taptotransfer.receiver
import android.content.Context
import android.util.AttributeSet
-import com.android.systemui.ripple.RippleShader
-import com.android.systemui.ripple.RippleView
+import com.android.systemui.surfaceeffects.ripple.RippleShader
+import com.android.systemui.surfaceeffects.ripple.RippleView
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
deleted file mode 100644
index 6de46483892b5..0000000000000
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.ripple
-
-/** A common utility functions that are used for computing [RippleShader]. */
-class RippleShaderUtilLibrary {
- //language=AGSL
- companion object {
- const val SHADER_LIB = """
- float triangleNoise(vec2 n) {
- n = fract(n * vec2(5.3987, 5.4421));
- n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
- float xy = n.x * n.y;
- return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
- }
- const float PI = 3.1415926535897932384626;
-
- float sparkles(vec2 uv, float t) {
- float n = triangleNoise(uv);
- float s = 0.0;
- for (float i = 0; i < 4; i += 1) {
- float l = i * 0.01;
- float h = l + 0.1;
- float o = smoothstep(n - l, h, n);
- o *= abs(sin(PI * o * (t + 0.55 * i)));
- s += o;
- }
- return s;
- }
-
- vec2 distort(vec2 p, float time, float distort_amount_radial,
- float distort_amount_xy) {
- float angle = atan(p.y, p.x);
- return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
- cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
- + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
- cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
- }"""
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index e0ead24dd382f..294bd997bd51a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -174,7 +174,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSFragment;
import com.android.systemui.qs.QSPanelController;
import com.android.systemui.recents.ScreenPinningRequest;
-import com.android.systemui.ripple.RippleShader.RippleShape;
import com.android.systemui.scrim.ScrimView;
import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.shade.CameraLauncher;
@@ -234,6 +233,7 @@ import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
import com.android.systemui.util.DumpUtilsKt;
import com.android.systemui.util.WallpaperController;
import com.android.systemui.util.concurrency.DelayableExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
index 48df15c78ea5a..93e78acc63fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleController.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import androidx.annotation.VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
index c7f0b7e0056eb..f558fee776e6a 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/MultiRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/MultiRippleView.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.content.Context
import android.graphics.Canvas
@@ -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 {
- const val TAG = "MultiRippleView"
+ 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/ripple/RippleAnimation.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
index aca9e254e4c3f..b2f8994ebf511 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
index 88122544c7cd5..ae73df201f8d1 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleAnimationConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
@@ -1,4 +1,4 @@
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
similarity index 69%
rename from packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index d2f3a6a7bee1b..a950d34513eec 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.graphics.PointF
import android.graphics.RuntimeShader
import android.util.MathUtils
+import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary
+import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
/**
* Shader class that renders an expanding ripple effect. The ripple contains three elements:
@@ -31,7 +33,7 @@ import android.util.MathUtils
* Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
*/
class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
- RuntimeShader(buildShader(rippleShape)) {
+ RuntimeShader(buildShader(rippleShape)) {
/** Shapes that the [RippleShader] supports. */
enum class RippleShape {
@@ -39,25 +41,30 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
ROUNDED_BOX,
ELLIPSE
}
- //language=AGSL
+ // language=AGSL
companion object {
- private const val SHADER_UNIFORMS = """uniform vec2 in_center;
- uniform vec2 in_size;
- uniform float in_progress;
- uniform float in_cornerRadius;
- uniform float in_thickness;
- uniform float in_time;
- uniform float in_distort_radial;
- uniform float in_distort_xy;
- uniform float in_fadeSparkle;
- uniform float in_fadeFill;
- uniform float in_fadeRing;
- uniform float in_blur;
- uniform float in_pixelDensity;
- layout(color) uniform vec4 in_color;
- uniform float in_sparkle_strength;"""
+ private const val SHADER_UNIFORMS =
+ """
+ uniform vec2 in_center;
+ uniform vec2 in_size;
+ uniform float in_progress;
+ uniform float in_cornerRadius;
+ uniform float in_thickness;
+ uniform float in_time;
+ uniform float in_distort_radial;
+ uniform float in_distort_xy;
+ uniform float in_fadeSparkle;
+ uniform float in_fadeFill;
+ uniform float in_fadeRing;
+ uniform float in_blur;
+ uniform float in_pixelDensity;
+ layout(color) uniform vec4 in_color;
+ uniform float in_sparkle_strength;
+ """
- private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) {
+ private const val SHADER_CIRCLE_MAIN =
+ """
+ vec4 main(vec2 p) {
vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
float radius = in_size.x * 0.5;
float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur);
@@ -73,7 +80,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
}
"""
- private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) {
+ private const val SHADER_ROUNDED_BOX_MAIN =
+ """
+ vec4 main(vec2 p) {
float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
in_thickness), in_blur);
float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius),
@@ -89,7 +98,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
}
"""
- private const val SHADER_ELLIPSE_MAIN = """vec4 main(vec2 p) {
+ private const val SHADER_ELLIPSE_MAIN =
+ """
+ vec4 main(vec2 p) {
vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur);
@@ -105,22 +116,31 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
}
"""
- private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
- SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF +
+ private const val CIRCLE_SHADER =
+ SHADER_UNIFORMS +
+ ShaderUtilLibrary.SHADER_LIB +
+ SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+ SdfShaderLibrary.CIRCLE_SDF +
SHADER_CIRCLE_MAIN
- private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS +
- RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
- SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN
- private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
- SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF +
+ private const val ROUNDED_BOX_SHADER =
+ SHADER_UNIFORMS +
+ ShaderUtilLibrary.SHADER_LIB +
+ SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+ SdfShaderLibrary.ROUNDED_BOX_SDF +
+ SHADER_ROUNDED_BOX_MAIN
+ private const val ELLIPSE_SHADER =
+ SHADER_UNIFORMS +
+ ShaderUtilLibrary.SHADER_LIB +
+ SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
+ SdfShaderLibrary.ELLIPSE_SDF +
SHADER_ELLIPSE_MAIN
private fun buildShader(rippleShape: RippleShape): String =
- when (rippleShape) {
- RippleShape.CIRCLE -> CIRCLE_SHADER
- RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER
- RippleShape.ELLIPSE -> ELLIPSE_SHADER
- }
+ when (rippleShape) {
+ RippleShape.CIRCLE -> CIRCLE_SHADER
+ RippleShape.ROUNDED_BOX -> ROUNDED_BOX_SHADER
+ RippleShape.ELLIPSE -> ELLIPSE_SHADER
+ }
private fun subProgress(start: Float, end: Float, progress: Float): Float {
val min = Math.min(start, end)
@@ -130,9 +150,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
}
}
- /**
- * Sets the center position of the ripple.
- */
+ /** Sets the center position of the ripple. */
fun setCenter(x: Float, y: Float) {
setFloatUniform("in_center", x, y)
}
@@ -144,21 +162,21 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
maxSize.y = height
}
- /**
- * Progress of the ripple. Float value between [0, 1].
- */
+ /** Progress of the ripple. Float value between [0, 1]. */
var progress: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
- setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg,
- /* height= */ maxSize.y * curvedProg)
+ setFloatUniform(
+ "in_size",
+ /* width= */ maxSize.x * curvedProg,
+ /* height= */ maxSize.y * curvedProg
+ )
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
- setFloatUniform("in_cornerRadius",
- Math.min(maxSize.x, maxSize.y) * curvedProg)
+ setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value))
@@ -175,18 +193,14 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
}
- /**
- * Play time since the start of the effect.
- */
+ /** Play time since the start of the effect. */
var time: Float = 0.0f
set(value) {
field = value
setFloatUniform("in_time", value)
}
- /**
- * A hex value representing the ripple color, in the format of ARGB
- */
+ /** A hex value representing the ripple color, in the format of ARGB */
var color: Int = 0xffffff
set(value) {
field = value
@@ -194,9 +208,9 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
}
/**
- * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus
- * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1
- * it's opaque white and looks the most grainy.
+ * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with
+ * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's
+ * opaque white and looks the most grainy.
*/
var sparkleStrength: Float = 0.0f
set(value) {
@@ -204,9 +218,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
setFloatUniform("in_sparkle_strength", value)
}
- /**
- * Distortion strength of the ripple. Expected value between[0, 1].
- */
+ /** Distortion strength of the ripple. Expected value between[0, 1]. */
var distortionStrength: Float = 0.0f
set(value) {
field = value
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index a6d79303962f6..299469494295b 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
@@ -26,7 +26,7 @@ import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.graphics.ColorUtils
-import com.android.systemui.ripple.RippleShader.RippleShape
+import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape
/**
* A generic expanding ripple effect.
@@ -98,15 +98,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
rippleShader.time = now.toFloat()
invalidate()
}
- animator.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- onAnimationEnd?.run()
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ onAnimationEnd?.run()
+ }
}
- })
+ )
animator.start()
}
- /** Set the color to be used for the ripple.
+ /**
+ * Set the color to be used for the ripple.
*
* The alpha value of the color will be applied to the ripple. The alpha range is [0-100].
*/
@@ -123,9 +126,7 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
rippleShader.rippleFill = rippleFill
}
- /**
- * Set the intensity of the sparkles.
- */
+ /** Set the intensity of the sparkles. */
fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength
}
@@ -143,20 +144,30 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
// active effect area. Values here should be kept in sync with the animation implementation
// in the ripple shader.
if (rippleShape == RippleShape.CIRCLE) {
- val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxWidth
+ val maskRadius =
+ (1 -
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress)) * maxWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else {
- val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxWidth * 2
- val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * maxHeight * 2
+ val maskWidth =
+ (1 -
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress)) * maxWidth * 2
+ val maskHeight =
+ (1 -
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress) *
+ (1 - rippleShader.progress)) * maxHeight * 2
canvas.drawRect(
- /* left= */ centerX - maskWidth,
- /* top= */ centerY - maskHeight,
- /* right= */ centerX + maskWidth,
- /* bottom= */ centerY + maskHeight,
- ripplePaint)
+ /* left= */ centerX - maskWidth,
+ /* top= */ centerY - maskHeight,
+ /* right= */ centerX + maskWidth,
+ /* bottom= */ centerY + maskHeight,
+ ripplePaint
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
rename to packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
index 5e256c6539924..8b2f46648fef1 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.shaderutil
/** Library class that contains 2D signed distance functions. */
class SdfShaderLibrary {
- //language=AGSL
+ // language=AGSL
companion object {
- const val CIRCLE_SDF = """
+ const val CIRCLE_SDF =
+ """
float sdCircle(vec2 p, float r) {
return (length(p)-r) / r;
}
@@ -34,7 +35,8 @@ class SdfShaderLibrary {
}
"""
- const val ROUNDED_BOX_SDF = """
+ const val ROUNDED_BOX_SDF =
+ """
float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
size *= 0.5;
cornerRadius *= 0.5;
@@ -58,7 +60,8 @@ class SdfShaderLibrary {
// Used non-trigonometry parametrization and Halley's method (iterative) for root finding.
// This is more expensive than the regular circle SDF, recommend to use the circle SDF if
// possible.
- const val ELLIPSE_SDF = """float sdEllipse(vec2 p, vec2 wh) {
+ const val ELLIPSE_SDF =
+ """float sdEllipse(vec2 p, vec2 wh) {
wh *= 0.5;
// symmetry
@@ -98,7 +101,8 @@ class SdfShaderLibrary {
}
"""
- const val SHADER_SDF_OPERATION_LIB = """
+ const val SHADER_SDF_OPERATION_LIB =
+ """
float soften(float d, float blur) {
float blurHalf = blur * 0.5;
return smoothstep(-blurHalf, blurHalf, d);
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
new file mode 100644
index 0000000000000..d78e0c153db8b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/shaderutil/ShaderUtilLibrary.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.shaderutil
+
+/** A common utility functions that are used for computing shaders. */
+class ShaderUtilLibrary {
+ // language=AGSL
+ companion object {
+ const val SHADER_LIB =
+ """
+ float triangleNoise(vec2 n) {
+ n = fract(n * vec2(5.3987, 5.4421));
+ n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
+ float xy = n.x * n.y;
+ // compute in [0..2[ and remap to [-1.0..1.0[
+ return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
+ }
+
+ const float PI = 3.1415926535897932384626;
+
+ float sparkles(vec2 uv, float t) {
+ float n = triangleNoise(uv);
+ float s = 0.0;
+ for (float i = 0; i < 4; i += 1) {
+ float l = i * 0.01;
+ float h = l + 0.1;
+ float o = smoothstep(n - l, h, n);
+ o *= abs(sin(PI * o * (t + 0.55 * i)));
+ s += o;
+ }
+ return s;
+ }
+
+ vec2 distort(vec2 p, float time, float distort_amount_radial,
+ float distort_amount_xy) {
+ float angle = atan(p.y, p.x);
+ return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
+ cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
+ + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
+ cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
+ }
+
+ // Return range [-1, 1].
+ vec3 hash(vec3 p) {
+ p = fract(p * vec3(.3456, .1234, .9876));
+ p += dot(p, p.yxz + 43.21);
+ p = (p.xxy + p.yxx) * p.zyx;
+ return (fract(sin(p) * 4567.1234567) - .5) * 2.;
+ }
+
+ // Skew factors (non-uniform).
+ const float SKEW = 0.3333333; // 1/3
+ const float UNSKEW = 0.1666667; // 1/6
+
+ // Return range roughly [-1,1].
+ // It's because the hash function (that returns a random gradient vector) returns
+ // different magnitude of vectors. Noise doesn't have to be in the precise range thus
+ // skipped normalize.
+ float simplex3d(vec3 p) {
+ // Skew the input coordinate, so that we get squashed cubical grid
+ vec3 s = floor(p + (p.x + p.y + p.z) * SKEW);
+
+ // Unskew back
+ vec3 u = s - (s.x + s.y + s.z) * UNSKEW;
+
+ // Unskewed coordinate that is relative to p, to compute the noise contribution
+ // based on the distance.
+ vec3 c0 = p - u;
+
+ // We have six simplices (in this case tetrahedron, since we are in 3D) that we
+ // could possibly in.
+ // Here, we are finding the correct tetrahedron (simplex shape), and traverse its
+ // four vertices (c0..3) when computing noise contribution.
+ // The way we find them is by comparing c0's x,y,z values.
+ // For example in 2D, we can find the triangle (simplex shape in 2D) that we are in
+ // by comparing x and y values. i.e. x>y lower, xy0>z0: (1,0,0), (1,1,0), (1,1,1)
+ // x0>z0>y0: (1,0,0), (1,0,1), (1,1,1)
+ // z0>x0>y0: (0,0,1), (1,0,1), (1,1,1)
+ // z0>y0>x0: (0,0,1), (0,1,1), (1,1,1)
+ // y0>z0>x0: (0,1,0), (0,1,1), (1,1,1)
+ // y0>x0>z0: (0,1,0), (1,1,0), (1,1,1)
+ //
+ // The rule is:
+ // * For offset1, set 1 at the max component, otherwise 0.
+ // * For offset2, set 0 at the min component, otherwise 1.
+ // * For offset3, set 1 for all.
+ //
+ // Encode x0-y0, y0-z0, z0-x0 in a vec3
+ vec3 en = c0 - c0.yzx;
+ // Each represents whether x0>y0, y0>z0, z0>x0
+ en = step(vec3(0.), en);
+ // en.zxy encodes z0>x0, x0>y0, y0>x0
+ vec3 offset1 = en * (1. - en.zxy); // find max
+ vec3 offset2 = 1. - en.zxy * (1. - en); // 1-(find min)
+ vec3 offset3 = vec3(1.);
+
+ vec3 c1 = c0 - offset1 + UNSKEW;
+ vec3 c2 = c0 - offset2 + UNSKEW * 2.;
+ vec3 c3 = c0 - offset3 + UNSKEW * 3.;
+
+ // Kernel summation: dot(max(0, r^2-d^2))^4, noise contribution)
+ //
+ // First compute d^2, squared distance to the point.
+ vec4 w; // w = max(0, r^2 - d^2))
+ w.x = dot(c0, c0);
+ w.y = dot(c1, c1);
+ w.z = dot(c2, c2);
+ w.w = dot(c3, c3);
+
+ // Noise contribution should decay to zero before they cross the simplex boundary.
+ // Usually r^2 is 0.5 or 0.6;
+ // 0.5 ensures continuity but 0.6 increases the visual quality for the application
+ // where discontinuity isn't noticeable.
+ w = max(0.6 - w, 0.);
+
+ // Noise contribution from each point.
+ vec4 nc;
+ nc.x = dot(hash(s), c0);
+ nc.y = dot(hash(s + offset1), c1);
+ nc.z = dot(hash(s + offset2), c2);
+ nc.w = dot(hash(s + offset3), c3);
+
+ nc *= w*w*w*w;
+
+ // Add all the noise contributions.
+ // Should multiply by the possible max contribution to adjust the range in [-1,1].
+ return dot(vec4(32.), nc);
+ }
+ """
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
new file mode 100644
index 0000000000000..5ac3aad749fc1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.turbulencenoise
+
+import android.graphics.BlendMode
+import android.graphics.Color
+
+/** Turbulence noise animation configuration. */
+data class TurbulenceNoiseAnimationConfig(
+ /** The number of grids that is used to generate noise. */
+ val gridCount: Float = DEFAULT_NOISE_GRID_COUNT,
+
+ /** Multiplier for the noise luma matte. Increase this for brighter effects. */
+ val luminosityMultiplier: Float = DEFAULT_LUMINOSITY_MULTIPLIER,
+
+ /**
+ * Noise move speed variables.
+ *
+ * Its sign determines the direction; magnitude determines the speed.
+ * ```
+ * - [noiseMoveSpeedX] positive: right to left; negative: left to right.
+ *
- [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom.
+ *
- [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it
+ * to add turbulence in place.
+ * ```
+ *
+ */
+ val noiseMoveSpeedX: Float = 0f,
+ val noiseMoveSpeedY: Float = 0f,
+ val noiseMoveSpeedZ: Float = DEFAULT_NOISE_SPEED_Z,
+
+ /** Color of the effect. */
+ var color: Int = DEFAULT_COLOR,
+ /** Background color of the effect. */
+ val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR,
+ val opacity: Int = DEFAULT_OPACITY,
+ val width: Float = 0f,
+ val height: Float = 0f,
+ val duration: Float = DEFAULT_NOISE_DURATION_IN_MILLIS,
+ val pixelDensity: Float = 1f,
+ val blendMode: BlendMode = DEFAULT_BLEND_MODE,
+ val onAnimationEnd: Runnable? = null
+) {
+ companion object {
+ const val DEFAULT_NOISE_DURATION_IN_MILLIS = 7500F
+ const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f
+ const val DEFAULT_NOISE_GRID_COUNT = 1.2f
+ const val DEFAULT_NOISE_SPEED_Z = 0.3f
+ const val DEFAULT_OPACITY = 150 // full opacity is 255.
+ const val DEFAULT_COLOR = Color.WHITE
+ const val DEFAULT_BACKGROUND_COLOR = Color.BLACK
+ val DEFAULT_BLEND_MODE = BlendMode.SRC_OVER
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
new file mode 100644
index 0000000000000..4c7e5f4c70937
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseController.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.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) {
+ turbulenceNoiseView.play(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
new file mode 100644
index 0000000000000..19c114d2693c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.turbulencenoise
+
+import android.graphics.RuntimeShader
+import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary
+import java.lang.Float.max
+
+/** Shader that renders turbulence simplex noise, with no octave. */
+class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) {
+ // language=AGSL
+ companion object {
+ private const val UNIFORMS =
+ """
+ uniform float in_gridNum;
+ uniform vec3 in_noiseMove;
+ uniform vec2 in_size;
+ uniform float in_aspectRatio;
+ uniform float in_opacity;
+ uniform float in_pixelDensity;
+ layout(color) uniform vec4 in_color;
+ layout(color) uniform vec4 in_backgroundColor;
+ """
+
+ private const val SHADER_LIB =
+ """
+ float getLuminosity(vec3 c) {
+ return 0.3*c.r + 0.59*c.g + 0.11*c.b;
+ }
+
+ vec3 maskLuminosity(vec3 dest, float lum) {
+ dest.rgb *= vec3(lum);
+ // Clip back into the legal range
+ dest = clamp(dest, vec3(0.), vec3(1.0));
+ return dest;
+ }
+ """
+
+ private const val MAIN_SHADER =
+ """
+ vec4 main(vec2 p) {
+ vec2 uv = p / in_size.xy;
+ uv.x *= in_aspectRatio;
+
+ vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
+ float luma = simplex3d(noiseP) * in_opacity;
+ vec3 mask = maskLuminosity(in_color.rgb, luma);
+ vec3 color = in_backgroundColor.rgb + mask * 0.6;
+
+ // Add dither with triangle distribution to avoid color banding. Ok to dither in the
+ // shader here as we are in gamma space.
+ float dither = triangleNoise(p * in_pixelDensity) / 255.;
+
+ // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to
+ // multiply rgb with a to get the correct result.
+ color = (color + dither.rrr) * in_color.a;
+ return vec4(color, in_color.a);
+ }
+ """
+
+ private const val TURBULENCE_NOISE_SHADER =
+ ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SHADER_LIB + MAIN_SHADER
+ }
+
+ /** Sets the number of grid for generating noise. */
+ fun setGridCount(gridNumber: Float = 1.0f) {
+ setFloatUniform("in_gridNum", gridNumber)
+ }
+
+ /**
+ * Sets the pixel density of the screen.
+ *
+ * Used it for noise dithering.
+ */
+ fun setPixelDensity(pixelDensity: Float) {
+ setFloatUniform("in_pixelDensity", pixelDensity)
+ }
+
+ /** Sets the noise color of the effect. */
+ fun setColor(color: Int) {
+ setColorUniform("in_color", color)
+ }
+
+ /** Sets the background color of the effect. */
+ fun setBackgroundColor(color: Int) {
+ setColorUniform("in_backgroundColor", color)
+ }
+
+ /**
+ * Sets the opacity to achieve fade in/ out of the animation.
+ *
+ * Expected value range is [1, 0].
+ */
+ fun setOpacity(opacity: Float) {
+ setFloatUniform("in_opacity", opacity)
+ }
+
+ /** Sets the size of the shader. */
+ fun setSize(width: Float, height: Float) {
+ setFloatUniform("in_size", width, height)
+ setFloatUniform("in_aspectRatio", width / max(height, 0.001f))
+ }
+
+ /** Sets noise move speed in x, y, and z direction. */
+ fun setNoiseMove(x: Float, y: Float, z: Float) {
+ setFloatUniform("in_noiseMove", x, y, z)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
new file mode 100644
index 0000000000000..8649d59245874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseView.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.turbulencenoise
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.core.graphics.ColorUtils
+import java.util.Random
+import kotlin.math.sin
+
+/** View that renders turbulence noise effect. */
+class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+
+ companion object {
+ private const val MS_TO_SEC = 0.001f
+ private const val TWO_PI = Math.PI.toFloat() * 2f
+ }
+
+ @VisibleForTesting val turbulenceNoiseShader = TurbulenceNoiseShader()
+ 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
+
+ init {
+ // Only visible during the animation.
+ 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
+ // if it's unsupported.
+ return
+ }
+
+ canvas.drawPaint(paint)
+ }
+
+ internal fun play(config: TurbulenceNoiseAnimationConfig) {
+ if (isPlaying) {
+ return // Ignore if the animation is playing.
+ }
+ visibility = VISIBLE
+ applyConfig(config)
+
+ // Add random offset to avoid same patterned noise.
+ val offsetX = random.nextFloat()
+ val offsetY = random.nextFloat()
+
+ animator.duration = config.duration.toLong()
+ animator.addUpdateListener { updateListener ->
+ val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+ // Remap [0,1] to [0, 2*PI]
+ val progress = TWO_PI * updateListener.animatedValue as Float
+
+ turbulenceNoiseShader.setNoiseMove(
+ offsetX + timeInSec * config.noiseMoveSpeedX,
+ offsetY + timeInSec * config.noiseMoveSpeedY,
+ timeInSec * config.noiseMoveSpeedZ
+ )
+
+ // Fade in and out the noise as the animation progress.
+ // TODO: replace it with a better curve
+ turbulenceNoiseShader.setOpacity(sin(TWO_PI - progress) * config.luminosityMultiplier)
+
+ invalidate()
+ }
+
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ visibility = INVISIBLE
+ config.onAnimationEnd?.run()
+ }
+ }
+ )
+ animator.start()
+ }
+
+ private fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
+ this.config = config
+ with(turbulenceNoiseShader) {
+ setGridCount(config.gridCount)
+ setColor(ColorUtils.setAlphaComponent(config.color, config.opacity))
+ setBackgroundColor(config.backgroundColor)
+ setSize(config.width, config.height)
+ setPixelDensity(config.pixelDensity)
+ }
+ paint.blendMode = config.blendMode
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index 2af055783c222..d1597149ce0b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -24,7 +24,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
-import com.android.systemui.ripple.RippleView
+import com.android.systemui.surfaceeffects.ripple.RippleView
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
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 a8f413848009e..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
@@ -25,7 +25,8 @@ import com.android.systemui.SysuiTestCase
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.ripple.MultiRippleController
+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 1ad2ca9b9db39..eb1f5e429182f 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
@@ -78,9 +78,10 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.ripple.MultiRippleView
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/ripple/MultiRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt
index 05512e5bf1ceb..0d19ab1db3909 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ripple/MultiRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/MultiRippleControllerTest.kt
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER
+import com.android.systemui.surfaceeffects.ripple.MultiRippleController.Companion.MAX_RIPPLE_NUMBER
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
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/ripple/RippleAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
index 7662282a04f44..756397a30e436 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleAnimationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color
import android.testing.AndroidTestingRunner
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt
index 2d2f4cc9edd25..1e5ab7e255996 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleViewTest.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.systemui.ripple
+package com.android.systemui.surfaceeffects.ripple
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -21,12 +21,10 @@ import com.android.systemui.SysuiTestCase
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
@SmallTest
@RunWith(AndroidTestingRunner::class)
class RippleViewTest : SysuiTestCase() {
- @Mock
private lateinit var rippleView: RippleView
@Before
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
new file mode 100644
index 0000000000000..d25c8c1a58991
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseControllerTest.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.turbulencenoise
+
+import android.graphics.Color
+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 TurbulenceNoiseControllerTest : SysuiTestCase() {
+ private val fakeSystemClock = FakeSystemClock()
+ // FakeExecutor is needed to run animator.
+ private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+ @Test
+ fun play_playsTurbulenceNoise() {
+ val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+ val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+ val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
+
+ fakeExecutor.execute {
+ turbulenceNoiseController.play(config)
+
+ assertThat(turbulenceNoiseView.isPlaying).isTrue()
+
+ fakeSystemClock.advanceTime(config.duration.toLong())
+
+ 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)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt
new file mode 100644
index 0000000000000..633aac0765029
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseViewTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.turbulencenoise
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+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 TurbulenceNoiseViewTest : SysuiTestCase() {
+
+ private val fakeSystemClock = FakeSystemClock()
+ // FakeExecutor is needed to run animator.
+ private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+ @Test
+ fun play_viewHasCorrectVisibility() {
+ val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+ val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+ assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+
+ fakeExecutor.execute {
+ turbulenceNoiseView.play(config)
+
+ assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
+
+ fakeSystemClock.advanceTime(config.duration.toLong())
+
+ assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
+ }
+ }
+
+ @Test
+ fun play_playsAnimation() {
+ val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
+ val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+ fakeExecutor.execute {
+ turbulenceNoiseView.play(config)
+
+ assertThat(turbulenceNoiseView.isPlaying).isTrue()
+ }
+ }
+
+ @Test
+ fun play_onEnd_triggersOnAnimationEnd() {
+ var animationEnd = false
+ val config =
+ TurbulenceNoiseAnimationConfig(
+ duration = 1000f,
+ onAnimationEnd = { animationEnd = true }
+ )
+ val turbulenceNoiseView = TurbulenceNoiseView(context, null)
+
+ fakeExecutor.execute {
+ turbulenceNoiseView.play(config)
+
+ assertThat(turbulenceNoiseView.isPlaying).isTrue()
+
+ fakeSystemClock.advanceTime(config.duration.toLong())
+
+ assertThat(animationEnd).isTrue()
+ }
+ }
+}