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:
@@ -44,7 +44,7 @@
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="@dimen/qs_media_session_height_expanded"
|
||||
@@ -53,6 +53,15 @@
|
||||
app:layout_constraintTop_toTopOf="@id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@id/album_art" />
|
||||
|
||||
<com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView
|
||||
android:id="@+id/turbulence_noise_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/qs_media_session_height_expanded"
|
||||
app:layout_constraintStart_toStartOf="@id/album_art"
|
||||
app:layout_constraintEnd_toEndOf="@id/album_art"
|
||||
app:layout_constraintTop_toTopOf="@id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@id/album_art" />
|
||||
|
||||
<!-- Guideline for output switcher -->
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/center_vertical_guideline"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
android:layout_width="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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||
|
||||
<!-- Turbulence noise must have the same constraint as the album art. -->
|
||||
<Constraint
|
||||
android:id="@+id/turbulence_noise_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/qs_media_session_height_collapsed"
|
||||
app:layout_constraintStart_toStartOf="@+id/album_art"
|
||||
app:layout_constraintEnd_toEndOf="@+id/album_art"
|
||||
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/header_title"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -37,6 +37,16 @@
|
||||
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||
|
||||
<!-- Turbulence noise must have the same constraint as the album art. -->
|
||||
<Constraint
|
||||
android:id="@+id/turbulence_noise_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/qs_media_session_height_expanded"
|
||||
app:layout_constraintStart_toStartOf="@+id/album_art"
|
||||
app:layout_constraintEnd_toEndOf="@+id/album_art"
|
||||
app:layout_constraintTop_toTopOf="@+id/album_art"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_art" />
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/header_title"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -29,7 +29,7 @@ import android.view.View
|
||||
import android.view.animation.PathInterpolator
|
||||
import com.android.internal.graphics.ColorUtils
|
||||
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
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import com.android.systemui.R
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
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.Command
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||
import com.android.systemui.statusbar.policy.BatteryController
|
||||
|
||||
@@ -30,7 +30,7 @@ import android.view.WindowManager;
|
||||
|
||||
import com.android.internal.logging.UiEvent;
|
||||
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.
|
||||
|
||||
@@ -33,9 +33,9 @@ import android.widget.TextView;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.animation.Interpolators;
|
||||
import com.android.systemui.ripple.RippleAnimationConfig;
|
||||
import com.android.systemui.ripple.RippleShader.RippleShape;
|
||||
import com.android.systemui.ripple.RippleView;
|
||||
import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
|
||||
import com.android.systemui.surfaceeffects.ripple.RippleShader.RippleShape;
|
||||
import com.android.systemui.surfaceeffects.ripple.RippleView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.Barrier
|
||||
import com.android.systemui.R
|
||||
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
|
||||
|
||||
private const val TAG = "MediaViewHolder"
|
||||
@@ -38,6 +39,8 @@ class MediaViewHolder constructor(itemView: View) {
|
||||
// Player information
|
||||
val albumView = itemView.requireViewById<ImageView>(R.id.album_art)
|
||||
val multiRippleView = itemView.requireViewById<MultiRippleView>(R.id.touch_ripple_view)
|
||||
val turbulenceNoiseView =
|
||||
itemView.requireViewById<TurbulenceNoiseView>(R.id.turbulence_noise_view)
|
||||
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
|
||||
val titleText = itemView.requireViewById<TextView>(R.id.header_title)
|
||||
val artistText = itemView.requireViewById<TextView>(R.id.header_artist)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}"""
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.ripple
|
||||
package com.android.systemui.surfaceeffects.ripple
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
@@ -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<RippleAnimation>()
|
||||
private val listeners = ArrayList<RipplesFinishedListener>()
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.android.systemui.ripple
|
||||
package com.android.systemui.surfaceeffects.ripple
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user