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