Merge changes from topic "noise-ripple" into tm-qpr-dev

* changes:
  Add turbulence noise effects after ripple in UMO.
  Add a turbulence noise shader for media player and ghost loading effects.
This commit is contained in:
Yein Jo
2022-11-14 23:28:57 +00:00
committed by Android (Google) Code Review
35 changed files with 948 additions and 171 deletions

View File

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

View File

@@ -22,7 +22,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.android.systemui.ripple.RippleView <com.android.systemui.surfaceeffects.ripple.RippleView
android:id="@+id/wireless_charging_ripple" android:id="@+id/wireless_charging_ripple"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ import android.view.View
import android.view.animation.PathInterpolator import android.view.animation.PathInterpolator
import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.ColorUtils
import com.android.systemui.animation.Interpolators import com.android.systemui.animation.Interpolators
import com.android.systemui.ripple.RippleShader import com.android.systemui.surfaceeffects.ripple.RippleShader
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f

View File

@@ -31,7 +31,7 @@ import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags 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.Command import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController

View File

@@ -30,7 +30,7 @@ import android.view.WindowManager;
import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger;
import com.android.systemui.ripple.RippleShader.RippleShape; import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
/** /**
* A WirelessChargingAnimation is a view containing view + animation for wireless charging. * A WirelessChargingAnimation is a view containing view + animation for wireless charging.

View File

@@ -33,9 +33,9 @@ import android.widget.TextView;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.Interpolators;
import com.android.systemui.ripple.RippleAnimationConfig; import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
import com.android.systemui.ripple.RippleShader.RippleShape; import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
import com.android.systemui.ripple.RippleView; import com.android.systemui.surfaceeffects.ripple.RippleView;
import java.text.NumberFormat; import java.text.NumberFormat;

View File

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

View File

@@ -29,7 +29,8 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.settingslib.Utils import com.android.settingslib.Utils
import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.monet.ColorScheme 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] * 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 context: Context,
private val mediaViewHolder: MediaViewHolder, private val mediaViewHolder: MediaViewHolder,
private val multiRippleController: MultiRippleController, private val multiRippleController: MultiRippleController,
private val turbulenceNoiseController: TurbulenceNoiseController,
animatingColorTransitionFactory: AnimatingColorTransitionFactory animatingColorTransitionFactory: AnimatingColorTransitionFactory
) { ) {
constructor( constructor(
context: Context, context: Context,
mediaViewHolder: MediaViewHolder, mediaViewHolder: MediaViewHolder,
multiRippleController: MultiRippleController, 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 bgColor = context.getColor(com.android.systemui.R.color.material_dynamic_secondary95)
val surfaceColor = val surfaceColor =
@@ -129,6 +138,7 @@ internal constructor(
mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList
mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary) mediaViewHolder.gutsViewHolder.setAccentPrimaryColor(accentPrimary)
multiRippleController.updateColor(accentPrimary) multiRippleController.updateColor(accentPrimary)
turbulenceNoiseController.updateNoiseColor(accentPrimary)
} }
val accentSecondary = val accentSecondary =

View File

@@ -31,6 +31,7 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.BlendMode;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.ColorMatrix; import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter; import android.graphics.ColorMatrixColorFilter;
@@ -64,6 +65,7 @@ import androidx.annotation.UiThread;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceId;
import com.android.settingslib.widget.AdaptiveIcon; 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.monet.Style;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager; 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.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.policy.KeyguardStateController; 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.ColorUtilKt;
import com.android.systemui.util.animation.TransitionLayout; import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.time.SystemClock; import com.android.systemui.util.time.SystemClock;
@@ -216,7 +221,9 @@ public class MediaControlPanel {
private boolean mShowBroadcastDialogButton = false; private boolean mShowBroadcastDialogButton = false;
private String mSwitchBroadcastApp; private String mSwitchBroadcastApp;
private MultiRippleController mMultiRippleController; private MultiRippleController mMultiRippleController;
private TurbulenceNoiseController mTurbulenceNoiseController;
private FeatureFlags mFeatureFlags; private FeatureFlags mFeatureFlags;
private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig = null;
/** /**
* Initialize a new control panel * Initialize a new control panel
@@ -394,9 +401,20 @@ public class MediaControlPanel {
AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); 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( mColorSchemeTransition = new ColorSchemeTransition(
mContext, mMediaViewHolder, mMultiRippleController); mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController);
mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter); mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
} }
@@ -1027,7 +1045,7 @@ public class MediaControlPanel {
/* maxWidth= */ maxSize, /* maxWidth= */ maxSize,
/* maxHeight= */ maxSize, /* maxHeight= */ maxSize,
/* pixelDensity= */ getContext().getResources().getDisplayMetrics().density, /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
mColorSchemeTransition.getAccentPrimary().getTargetColor(), mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
/* opacity= */ 100, /* opacity= */ 100,
/* shouldFillRipple= */ false, /* shouldFillRipple= */ false,
/* sparkleStrength= */ 0f, /* 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) { private void clearButton(final ImageButton button) {
button.setImageDrawable(null); button.setImageDrawable(null);
button.setContentDescription(null); button.setContentDescription(null);

View File

@@ -18,8 +18,8 @@ package com.android.systemui.media.taptotransfer.receiver
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import com.android.systemui.ripple.RippleShader import com.android.systemui.surfaceeffects.ripple.RippleShader
import com.android.systemui.ripple.RippleView import com.android.systemui.surfaceeffects.ripple.RippleView
/** /**
* An expanding ripple effect for the media tap-to-transfer receiver chip. * An expanding ripple effect for the media tap-to-transfer receiver chip.

View File

@@ -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;
}"""
}
}

View File

@@ -174,7 +174,6 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.QSFragment; import com.android.systemui.qs.QSFragment;
import com.android.systemui.qs.QSPanelController; import com.android.systemui.qs.QSPanelController;
import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.ripple.RippleShader.RippleShape;
import com.android.systemui.scrim.ScrimView; import com.android.systemui.scrim.ScrimView;
import com.android.systemui.settings.brightness.BrightnessSliderController; import com.android.systemui.settings.brightness.BrightnessSliderController;
import com.android.systemui.shade.CameraLauncher; 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.policy.UserSwitcherController;
import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.statusbar.window.StatusBarWindowStateController; 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.DumpUtilsKt;
import com.android.systemui.util.WallpaperController; import com.android.systemui.util.WallpaperController;
import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.DelayableExecutor;

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
@@ -31,11 +31,21 @@ import android.view.View
class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
internal val ripples = ArrayList<RippleAnimation>() internal val ripples = ArrayList<RippleAnimation>()
private val listeners = ArrayList<RipplesFinishedListener>()
private val ripplePaint = Paint() private val ripplePaint = Paint()
private var isWarningLogged = false private var isWarningLogged = false
companion object { 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?) { override fun onDraw(canvas: Canvas?) {
@@ -62,6 +72,10 @@ class MultiRippleView(context: Context?, attrs: AttributeSet?) : View(context, a
shouldInvalidate = shouldInvalidate || anim.isPlaying() shouldInvalidate = shouldInvalidate || anim.isPlaying()
} }
if (shouldInvalidate) invalidate() if (shouldInvalidate) {
invalidate()
} else { // Nothing is playing.
listeners.forEach { listener -> listener.onRipplesFinish() }
}
} }
} }

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter

View File

@@ -1,4 +1,4 @@
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color import android.graphics.Color

View File

@@ -13,11 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.graphics.PointF import android.graphics.PointF
import android.graphics.RuntimeShader import android.graphics.RuntimeShader
import android.util.MathUtils 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: * Shader class that renders an expanding ripple effect. The ripple contains three elements:
@@ -39,9 +41,11 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
ROUNDED_BOX, ROUNDED_BOX,
ELLIPSE ELLIPSE
} }
//language=AGSL // language=AGSL
companion object { companion object {
private const val SHADER_UNIFORMS = """uniform vec2 in_center; private const val SHADER_UNIFORMS =
"""
uniform vec2 in_center;
uniform vec2 in_size; uniform vec2 in_size;
uniform float in_progress; uniform float in_progress;
uniform float in_cornerRadius; uniform float in_cornerRadius;
@@ -55,9 +59,12 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
uniform float in_blur; uniform float in_blur;
uniform float in_pixelDensity; uniform float in_pixelDensity;
layout(color) uniform vec4 in_color; layout(color) uniform vec4 in_color;
uniform float in_sparkle_strength;""" 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); vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
float radius = in_size.x * 0.5; float radius = in_size.x * 0.5;
float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur); 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, float sparkleRing = soften(roundedBoxRing(p-in_center, in_size, in_cornerRadius,
in_thickness), in_blur); in_thickness), in_blur);
float inside = soften(sdRoundedBox(p-in_center, in_size * 1.2, in_cornerRadius), 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); 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); float sparkleRing = soften(ellipseRing(p_distorted-in_center, in_size), in_blur);
@@ -105,14 +116,23 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
} }
""" """
private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + private const val CIRCLE_SHADER =
SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.CIRCLE_SDF + SHADER_UNIFORMS +
ShaderUtilLibrary.SHADER_LIB +
SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
SdfShaderLibrary.CIRCLE_SDF +
SHADER_CIRCLE_MAIN SHADER_CIRCLE_MAIN
private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS + private const val ROUNDED_BOX_SHADER =
RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SHADER_UNIFORMS +
SdfShaderLibrary.ROUNDED_BOX_SDF + SHADER_ROUNDED_BOX_MAIN ShaderUtilLibrary.SHADER_LIB +
private const val ELLIPSE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB +
SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SdfShaderLibrary.ELLIPSE_SDF + 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 SHADER_ELLIPSE_MAIN
private fun buildShader(rippleShape: RippleShape): String = private fun buildShader(rippleShape: RippleShape): String =
@@ -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) { fun setCenter(x: Float, y: Float) {
setFloatUniform("in_center", x, y) setFloatUniform("in_center", x, y)
} }
@@ -144,21 +162,21 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
maxSize.y = height 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 var progress: Float = 0.0f
set(value) { set(value) {
field = value field = value
setFloatUniform("in_progress", value) setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value) val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
setFloatUniform("in_size", /* width= */ maxSize.x * curvedProg, setFloatUniform(
/* height= */ maxSize.y * curvedProg) "in_size",
/* width= */ maxSize.x * curvedProg,
/* height= */ maxSize.y * curvedProg
)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f) setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values. // radius should not exceed width and height values.
setFloatUniform("in_cornerRadius", setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
Math.min(maxSize.x, maxSize.y) * curvedProg)
setFloatUniform("in_blur", MathUtils.lerp(1.25f, 0.5f, value)) 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)) 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 var time: Float = 0.0f
set(value) { set(value) {
field = value field = value
setFloatUniform("in_time", 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 var color: Int = 0xffffff
set(value) { set(value) {
field = 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 * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus with
* with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 * strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 it's
* it's opaque white and looks the most grainy. * opaque white and looks the most grainy.
*/ */
var sparkleStrength: Float = 0.0f var sparkleStrength: Float = 0.0f
set(value) { set(value) {
@@ -204,9 +218,7 @@ class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.C
setFloatUniform("in_sparkle_strength", value) 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 var distortionStrength: Float = 0.0f
set(value) { set(value) {
field = value field = value

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
@@ -26,7 +26,7 @@ import android.graphics.Paint
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.core.graphics.ColorUtils 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. * A generic expanding ripple effect.
@@ -98,15 +98,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
rippleShader.time = now.toFloat() rippleShader.time = now.toFloat()
invalidate() invalidate()
} }
animator.addListener(object : AnimatorListenerAdapter() { animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
onAnimationEnd?.run() onAnimationEnd?.run()
} }
}) }
)
animator.start() 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]. * 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 rippleShader.rippleFill = rippleFill
} }
/** /** Set the intensity of the sparkles. */
* Set the intensity of the sparkles.
*/
fun setSparkleStrength(strength: Float) { fun setSparkleStrength(strength: Float) {
rippleShader.sparkleStrength = strength 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 // active effect area. Values here should be kept in sync with the animation implementation
// in the ripple shader. // in the ripple shader.
if (rippleShape == RippleShape.CIRCLE) { if (rippleShape == RippleShape.CIRCLE) {
val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * val maskRadius =
(1 -
(1 - rippleShader.progress) *
(1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxWidth (1 - rippleShader.progress)) * maxWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint) canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else { } else {
val maskWidth = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * val maskWidth =
(1 -
(1 - rippleShader.progress) *
(1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxWidth * 2 (1 - rippleShader.progress)) * maxWidth * 2
val maskHeight = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) * val maskHeight =
(1 -
(1 - rippleShader.progress) *
(1 - rippleShader.progress) *
(1 - rippleShader.progress)) * maxHeight * 2 (1 - rippleShader.progress)) * maxHeight * 2
canvas.drawRect( canvas.drawRect(
/* left= */ centerX - maskWidth, /* left= */ centerX - maskWidth,
/* top= */ centerY - maskHeight, /* top= */ centerY - maskHeight,
/* right= */ centerX + maskWidth, /* right= */ centerX + maskWidth,
/* bottom= */ centerY + maskHeight, /* bottom= */ centerY + maskHeight,
ripplePaint) ripplePaint
)
} }
} }
} }

View File

@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.shaderutil
/** Library class that contains 2D signed distance functions. */ /** Library class that contains 2D signed distance functions. */
class SdfShaderLibrary { class SdfShaderLibrary {
//language=AGSL // language=AGSL
companion object { companion object {
const val CIRCLE_SDF = """ const val CIRCLE_SDF =
"""
float sdCircle(vec2 p, float r) { float sdCircle(vec2 p, float r) {
return (length(p)-r) / 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) { float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
size *= 0.5; size *= 0.5;
cornerRadius *= 0.5; cornerRadius *= 0.5;
@@ -58,7 +60,8 @@ class SdfShaderLibrary {
// Used non-trigonometry parametrization and Halley's method (iterative) for root finding. // 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 // This is more expensive than the regular circle SDF, recommend to use the circle SDF if
// possible. // possible.
const val ELLIPSE_SDF = """float sdEllipse(vec2 p, vec2 wh) { const val ELLIPSE_SDF =
"""float sdEllipse(vec2 p, vec2 wh) {
wh *= 0.5; wh *= 0.5;
// symmetry // 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 soften(float d, float blur) {
float blurHalf = blur * 0.5; float blurHalf = blur * 0.5;
return smoothstep(-blurHalf, blurHalf, d); return smoothstep(-blurHalf, blurHalf, d);

View File

@@ -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, x<y, upper triangle.
// Same applies in 3D.
//
// Below indicates the offsets (or offset directions) when c0=(x0,y0,z0)
// x0>y0>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);
}
"""
}
}

View File

@@ -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. <ul>
* ```
* <li> [noiseMoveSpeedX] positive: right to left; negative: left to right.
* <li> [noiseMoveSpeedY] positive: bottom to top; negative: top to bottom.
* <li> [noiseMoveSpeedZ] its sign doesn't matter much, as it moves in Z direction. Use it
* to add turbulence in place.
* ```
* </ul>
*/
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
}
}

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags 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.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController

View File

@@ -25,7 +25,8 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.GutsViewHolder
import com.android.systemui.media.controls.models.player.MediaViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.monet.ColorScheme 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 junit.framework.Assert.assertEquals
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@@ -62,6 +63,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() {
@Mock private lateinit var mediaViewHolder: MediaViewHolder @Mock private lateinit var mediaViewHolder: MediaViewHolder
@Mock private lateinit var gutsViewHolder: GutsViewHolder @Mock private lateinit var gutsViewHolder: GutsViewHolder
@Mock private lateinit var multiRippleController: MultiRippleController @Mock private lateinit var multiRippleController: MultiRippleController
@Mock private lateinit var turbulenceNoiseController: TurbulenceNoiseController
@JvmField @Rule val mockitoRule = MockitoJUnit.rule() @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
@@ -76,6 +78,7 @@ class ColorSchemeTransitionTest : SysuiTestCase() {
context, context,
mediaViewHolder, mediaViewHolder,
multiRippleController, multiRippleController,
turbulenceNoiseController,
animatingColorTransitionFactory animatingColorTransitionFactory
) )

View File

@@ -78,9 +78,10 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.media.dialog.MediaOutputDialogFactory import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager
import com.android.systemui.ripple.MultiRippleView
import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.policy.KeyguardStateController 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.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.KotlinArgumentCaptor
@@ -178,6 +179,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
private lateinit var dismiss: FrameLayout private lateinit var dismiss: FrameLayout
private lateinit var dismissText: TextView private lateinit var dismissText: TextView
private lateinit var multiRippleView: MultiRippleView private lateinit var multiRippleView: MultiRippleView
private lateinit var turbulenceNoiseView: TurbulenceNoiseView
private lateinit var session: MediaSession private lateinit var session: MediaSession
private lateinit var device: MediaDeviceData private lateinit var device: MediaDeviceData
@@ -382,6 +384,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
} }
multiRippleView = MultiRippleView(context, null) multiRippleView = MultiRippleView(context, null)
turbulenceNoiseView = TurbulenceNoiseView(context, null)
whenever(viewHolder.player).thenReturn(view) whenever(viewHolder.player).thenReturn(view)
whenever(viewHolder.appIcon).thenReturn(appIcon) whenever(viewHolder.appIcon).thenReturn(appIcon)
@@ -425,6 +428,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier) whenever(viewHolder.actionsTopBarrier).thenReturn(actionsTopBarrier)
whenever(viewHolder.multiRippleView).thenReturn(multiRippleView) whenever(viewHolder.multiRippleView).thenReturn(multiRippleView)
whenever(viewHolder.turbulenceNoiseView).thenReturn(turbulenceNoiseView)
} }
/** Initialize elements for the recommendation view holder */ /** Initialize elements for the recommendation view holder */

View File

@@ -14,13 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color import android.graphics.Color
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.surfaceeffects.ripple
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class MultiRippleViewTest : SysuiTestCase() {
private val fakeSystemClock = FakeSystemClock()
// FakeExecutor is needed to run animator.
private val fakeExecutor = FakeExecutor(fakeSystemClock)
@Test
fun onRippleFinishes_triggersRippleFinished() {
val multiRippleView = MultiRippleView(context, null)
val multiRippleController = MultiRippleController(multiRippleView)
val rippleAnimationConfig = RippleAnimationConfig(duration = 1000L)
var isTriggered = false
val listener =
object : MultiRippleView.Companion.RipplesFinishedListener {
override fun onRipplesFinish() {
isTriggered = true
}
}
multiRippleView.addRipplesFinishedListener(listener)
fakeExecutor.execute {
val rippleAnimation = RippleAnimation(rippleAnimationConfig)
multiRippleController.play(rippleAnimation)
fakeSystemClock.advanceTime(rippleAnimationConfig.duration)
assertThat(isTriggered).isTrue()
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.graphics.Color import android.graphics.Color
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.ripple package com.android.systemui.surfaceeffects.ripple
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
@@ -21,12 +21,10 @@ import com.android.systemui.SysuiTestCase
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
class RippleViewTest : SysuiTestCase() { class RippleViewTest : SysuiTestCase() {
@Mock
private lateinit var rippleView: RippleView private lateinit var rippleView: RippleView
@Before @Before

View File

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

View File

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