summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt174
-rw-r--r--packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt80
-rw-r--r--packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt77
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt46
11 files changed, 342 insertions, 141 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index fef7383634f0..1c574808e0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -75,12 +75,12 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
}
private var radius: Float = 0f
set(value) {
- rippleShader.radius = value
+ rippleShader.setMaxSize(value * 2f, value * 2f)
field = value
}
private var origin: PointF = PointF()
set(value) {
- rippleShader.origin = value
+ rippleShader.setCenter(value.x, value.y)
field = value
}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
index 8292e52b1ffd..da675de2b9ec 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/charging/WiredChargingRippleController.kt
@@ -19,7 +19,6 @@ package com.android.systemui.charging
import android.content.Context
import android.content.res.Configuration
import android.graphics.PixelFormat
-import android.graphics.PointF
import android.os.SystemProperties
import android.util.DisplayMetrics
import android.view.View
@@ -85,7 +84,7 @@ class WiredChargingRippleController @Inject constructor(
private var debounceLevel = 0
@VisibleForTesting
- var rippleView: RippleView = RippleView(context, attrs = null)
+ var rippleView: RippleView = RippleView(context, attrs = null).also { it.setupShader() }
init {
pluggedIn = batteryController.isPluggedIn
@@ -177,20 +176,25 @@ class WiredChargingRippleController @Inject constructor(
context.display.getRealMetrics(displayMetrics)
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
- rippleView.radius = Integer.max(width, height).toFloat()
- rippleView.origin = when (RotationUtils.getExactRotation(context)) {
+ val maxDiameter = Integer.max(width, height) * 2f
+ rippleView.setMaxSize(maxDiameter, maxDiameter)
+ when (RotationUtils.getExactRotation(context)) {
RotationUtils.ROTATION_LANDSCAPE -> {
- PointF(width * normalizedPortPosY, height * (1 - normalizedPortPosX))
+ rippleView.setCenter(
+ width * normalizedPortPosY, height * (1 - normalizedPortPosX))
}
RotationUtils.ROTATION_UPSIDE_DOWN -> {
- PointF(width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
+ rippleView.setCenter(
+ width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
}
RotationUtils.ROTATION_SEASCAPE -> {
- PointF(width * (1 - normalizedPortPosY), height * normalizedPortPosX)
+ rippleView.setCenter(
+ width * (1 - normalizedPortPosY), height * normalizedPortPosX)
}
else -> {
// ROTATION_NONE
- PointF(width * normalizedPortPosX, height * normalizedPortPosY)
+ rippleView.setCenter(
+ width * normalizedPortPosX, height * normalizedPortPosY)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index f6368ee19e8f..65400c22ebd7 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -21,7 +21,6 @@ import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
-import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
@@ -34,6 +33,7 @@ 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.RippleShader;
import com.android.systemui.ripple.RippleView;
import java.text.NumberFormat;
@@ -138,6 +138,8 @@ public class WirelessChargingLayout extends FrameLayout {
animatorSetScrim.start();
mRippleView = findViewById(R.id.wireless_charging_ripple);
+ // TODO: Make rounded box shape if the device is tablet.
+ mRippleView.setupShader(RippleShader.RippleShape.CIRCLE);
OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
@@ -230,11 +232,11 @@ public class WirelessChargingLayout extends FrameLayout {
if (mRippleView != null) {
int width = getMeasuredWidth();
int height = getMeasuredHeight();
- mRippleView.setColor(
- Utils.getColorAttr(mRippleView.getContext(),
- android.R.attr.colorAccent).getDefaultColor());
- mRippleView.setOrigin(new PointF(width / 2, height / 2));
- mRippleView.setRadius(Math.max(width, height) * 0.5f);
+ mRippleView.setCenter(width * 0.5f, height * 0.5f);
+ float maxSize = Math.max(width, height);
+ mRippleView.setMaxSize(maxSize, maxSize);
+ mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
+ android.R.attr.colorAccent).getDefaultColor());
}
super.onLayout(changed, left, top, right, bottom);
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 99a5b8b2d450..5dc65ff194b9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -19,7 +19,6 @@ package com.android.systemui.media.taptotransfer.receiver
import android.annotation.SuppressLint
import android.app.StatusBarManager
import android.content.Context
-import android.graphics.PointF
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.MediaRoute2Info
@@ -201,10 +200,10 @@ class MediaTttChipControllerReceiver @Inject constructor(
val height = windowBounds.height()
val width = windowBounds.width()
- rippleView.radius = height / 5f
+ val maxDiameter = height / 2.5f
+ rippleView.setMaxSize(maxDiameter, maxDiameter)
// Center the ripple on the bottom of the screen in the middle.
- rippleView.origin = PointF(/* x= */ width / 2f, /* y= */ height.toFloat())
-
+ rippleView.setCenter(width * 0.5f, height.toFloat())
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
val colorWithAlpha = ColorUtils.setAlphaComponent(color, 70)
rippleView.setColor(colorWithAlpha)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index fed546bf52c2..8fd4440fe7a5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -23,10 +23,9 @@ import com.android.systemui.ripple.RippleView
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
*/
-class ReceiverChipRippleView(
- context: Context?, attrs: AttributeSet?
-) : RippleView(context, attrs) {
+class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleView(context, attrs) {
init {
+ setupShader()
setRippleFill(true)
duration = 3000L
}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
index 93a2efc4e96d..f4b94ede3bd9 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
@@ -20,92 +20,83 @@ import android.graphics.RuntimeShader
import android.util.MathUtils
/**
- * Shader class that renders an expanding charging ripple effect. A charging ripple contains
- * three elements:
- * 1. an expanding filled circle that appears in the beginning and quickly fades away
+ * Shader class that renders an expanding ripple effect. The ripple contains three elements:
+ *
+ * 1. an expanding filled [RippleShape] that appears in the beginning and quickly fades away
* 2. an expanding ring that appears throughout the effect
* 3. an expanding ring-shaped area that reveals noise over #2.
*
+ * The ripple shader will be default to the circle shape if not specified.
+ *
* Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java.
*/
-class RippleShader internal constructor() : RuntimeShader(SHADER) {
+class RippleShader internal constructor(rippleShape: RippleShape = RippleShape.CIRCLE) :
+ RuntimeShader(buildShader(rippleShape)) {
+
+ /** Shapes that the [RippleShader] supports. */
+ enum class RippleShape {
+ CIRCLE,
+ ROUNDED_BOX
+ }
+
companion object {
- private const val SHADER_UNIFORMS = """uniform vec2 in_origin;
+ private const val SHADER_UNIFORMS = """uniform vec2 in_center;
+ uniform vec2 in_size;
uniform float in_progress;
- uniform float in_maxRadius;
+ 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_radius;
uniform float in_fadeSparkle;
- uniform float in_fadeCircle;
+ 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_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 threshold(float v, float l, float h) {
- return step(l, v) * (1.0 - step(h, v));
- }
-
- 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;
- }
-
- float softCircle(vec2 uv, vec2 xy, float radius, float blur) {
- float blurHalf = blur * 0.5;
- float d = distance(uv, xy);
- return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);
- }
-
- float softRing(vec2 uv, vec2 xy, float radius, float blur) {
- float thickness_half = radius * 0.25;
- float circle_outer = softCircle(uv, xy, radius + thickness_half, blur);
- float circle_inner = softCircle(uv, xy, radius - thickness_half, blur);
- return circle_outer - circle_inner;
- }
-
- vec2 distort(vec2 p, vec2 origin, float time,
- float distort_amount_radial, float distort_amount_xy) {
- float2 distance = origin - p;
- float angle = atan(distance.y, distance.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;
- }"""
- private const val SHADER_MAIN = """vec4 main(vec2 p) {
- vec2 p_distorted = distort(p, in_origin, in_time, in_distort_radial,
- in_distort_xy);
-
- // Draw shapes
- float sparkleRing = softRing(p_distorted, in_origin, in_radius, in_blur);
- float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
- * sparkleRing * in_fadeSparkle;
- float circle = softCircle(p_distorted, in_origin, in_radius * 1.2, in_blur);
- float rippleAlpha = max(circle * in_fadeCircle,
- softRing(p_distorted, in_origin, in_radius, in_blur) * in_fadeRing) * 0.45;
- vec4 ripple = in_color * rippleAlpha;
- return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
- }"""
- private const val SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN
+
+ private const val SHADER_CIRCLE_MAIN = """vec4 main(vec2 p) {
+ // distortion only applies to circle
+ vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
+ float radius = in_size.x * 0.5;
+
+ float sparkleRing = softRing(p_distorted, in_center, radius, in_blur);
+ float inside = softCircle(p_distorted, in_center, radius * 1.2, in_blur);
+ float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
+ * (1.-sparkleRing) * in_fadeSparkle;
+
+ float rippleInsideAlpha = (1.-inside) * in_fadeFill;
+ float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
+ float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+ vec4 ripple = in_color * rippleAlpha;
+ return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
+ }
+ """
+
+ private const val SHADER_ROUNDED_BOX_MAIN = """vec4 main(vec2 p) {
+ float sparkleRing = softRoundedBoxRing(p, in_center, in_size, in_cornerRadius,
+ in_thickness, in_blur);
+ float inside = softRoundedBox(p, in_center, in_size * 1.2, in_cornerRadius, in_blur);
+ float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
+ * (1.-sparkleRing) * in_fadeSparkle;
+
+ float rippleInsideAlpha = (1.-inside) * in_fadeFill;
+ float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
+ float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+ vec4 ripple = in_color * rippleAlpha;
+ return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
+ }
+ """
+
+ private const val CIRCLE_SHADER = SHADER_UNIFORMS + RippleShaderUtilLibrary.SHADER_LIB +
+ SdfShaderLibrary.CIRCLE_SDF + SHADER_CIRCLE_MAIN
+ private const val ROUNDED_BOX_SHADER = SHADER_UNIFORMS +
+ RippleShaderUtilLibrary.SHADER_LIB + SdfShaderLibrary.ROUNDED_BOX_SDF +
+ SHADER_ROUNDED_BOX_MAIN
+
+ private fun buildShader(rippleShape: RippleShape): String =
+ if (rippleShape == RippleShape.CIRCLE) CIRCLE_SHADER else ROUNDED_BOX_SHADER
private fun subProgress(start: Float, end: Float, progress: Float): Float {
val min = Math.min(start, end)
@@ -116,22 +107,18 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
}
/**
- * Maximum radius of the ripple.
+ * Sets the center position of the ripple.
*/
- var radius: Float = 0.0f
- set(value) {
- field = value
- setFloatUniform("in_maxRadius", value)
- }
+ fun setCenter(x: Float, y: Float) {
+ setFloatUniform("in_center", x, y)
+ }
- /**
- * Origin coordinate of the ripple.
- */
- var origin: PointF = PointF()
- set(value) {
- field = value
- setFloatUniform("in_origin", value.x, value.y)
- }
+ /** Max width of the ripple. */
+ private var maxSize: PointF = PointF()
+ fun setMaxSize(width: Float, height: Float) {
+ maxSize.x = width
+ maxSize.y = height
+ }
/**
* Progress of the ripple. Float value between [0, 1].
@@ -140,20 +127,27 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
set(value) {
field = value
setFloatUniform("in_progress", value)
- setFloatUniform("in_radius",
- (1 - (1 - value) * (1 - value) * (1 - value))* radius)
+ val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
+
+ 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_blur", MathUtils.lerp(1.25f, 0.5f, value))
val fadeIn = subProgress(0f, 0.1f, value)
val fadeOutNoise = subProgress(0.4f, 1f, value)
var fadeOutRipple = 0f
- var fadeCircle = 0f
+ var fadeFill = 0f
if (!rippleFill) {
- fadeCircle = subProgress(0f, 0.2f, value)
+ fadeFill = subProgress(0f, 0.6f, value)
fadeOutRipple = subProgress(0.3f, 1f, value)
}
setFloatUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
- setFloatUniform("in_fadeCircle", 1 - fadeCircle)
+ setFloatUniform("in_fadeFill", 1 - fadeFill)
setFloatUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
}
@@ -169,7 +163,7 @@ class RippleShader internal constructor() : RuntimeShader(SHADER) {
/**
* A hex value representing the ripple color, in the format of ARGB
*/
- var color: Int = 0xffffff.toInt()
+ var color: Int = 0xffffff
set(value) {
field = value
setColorUniform("in_color", value)
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
new file mode 100644
index 000000000000..2dbc0ba2d8f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShaderUtilLibrary.kt
@@ -0,0 +1,51 @@
+/*
+ * 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 {
+ companion object {
+ const val SHADER_LIB = """float triangleNoise(vec2 n) {
+ n = fract(n * vec2(5.3987, 5.4421));
+ n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));
+ float xy = n.x * n.y;
+ return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;
+ }
+ const float PI = 3.1415926535897932384626;
+
+ float sparkles(vec2 uv, float t) {
+ float n = triangleNoise(uv);
+ float s = 0.0;
+ for (float i = 0; i < 4; i += 1) {
+ float l = i * 0.01;
+ float h = l + 0.1;
+ float o = smoothstep(n - l, h, n);
+ o *= abs(sin(PI * o * (t + 0.55 * i)));
+ s += o;
+ }
+ return s;
+ }
+
+ vec2 distort(vec2 p, float time, float distort_amount_radial,
+ float distort_amount_xy) {
+ float angle = atan(p.y, p.x);
+ return p + vec2(sin(angle * 8 + time * 0.003 + 1.641),
+ cos(angle * 5 + 2.14 + time * 0.00412)) * distort_amount_radial
+ + vec2(sin(p.x * 0.01 + time * 0.00215 + 0.8123),
+ cos(p.y * 0.01 + time * 0.005931)) * distort_amount_xy;
+ }"""
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
index fc52464ecf85..83d9f2da1db1 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
@@ -23,39 +23,42 @@ import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Paint
-import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
+import com.android.systemui.ripple.RippleShader.RippleShape
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
+private const val RIPPLE_DEFAULT_COLOR: Int = 0xffffffff.toInt()
/**
- * A generic expanding ripple effect. To trigger the ripple expansion, set [radius] and [origin],
- * then call [startRipple].
+ * A generic expanding ripple effect.
+ *
+ * Set up the shader with a desired [RippleShape] using [setupShader], [setMaxSize] and [setCenter],
+ * then call [startRipple] to trigger the ripple expansion.
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
- private val rippleShader = RippleShader()
- private val defaultColor: Int = 0xffffffff.toInt()
+
+ private lateinit var rippleShader: RippleShader
+ private lateinit var rippleShape: RippleShape
private val ripplePaint = Paint()
var rippleInProgress: Boolean = false
- var radius: Float = 0.0f
- set(value) {
- rippleShader.radius = value
- field = value
- }
- var origin: PointF = PointF()
- set(value) {
- rippleShader.origin = value
- field = value
- }
var duration: Long = 1750
- init {
- rippleShader.color = defaultColor
- rippleShader.progress = 0f
- rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
- ripplePaint.shader = rippleShader
+ private var maxWidth: Float = 0.0f
+ private var maxHeight: Float = 0.0f
+ fun setMaxSize(maxWidth: Float, maxHeight: Float) {
+ this.maxWidth = maxWidth
+ this.maxHeight = maxHeight
+ rippleShader.setMaxSize(maxWidth, maxHeight)
+ }
+
+ private var centerX: Float = 0.0f
+ private var centerY: Float = 0.0f
+ fun setCenter(x: Float, y: Float) {
+ this.centerX = x
+ this.centerY = y
+ rippleShader.setCenter(x, y)
}
override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -68,6 +71,18 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
super.onAttachedToWindow()
}
+ /** Initializes the shader. Must be called before [startRipple]. */
+ fun setupShader(rippleShape: RippleShape = RippleShape.CIRCLE) {
+ this.rippleShape = rippleShape
+ rippleShader = RippleShader(rippleShape)
+
+ rippleShader.color = RIPPLE_DEFAULT_COLOR
+ rippleShader.progress = 0f
+ rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
+
+ ripplePaint.shader = rippleShader
+ }
+
@JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (rippleInProgress) {
@@ -113,11 +128,24 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
// if it's unsupported.
return
}
- // To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
- // the active effect area. Values here should be kept in sync with the
- // animation implementation in the ripple shader.
- val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
- (1 - rippleShader.progress)) * radius * 2
- canvas.drawCircle(origin.x, origin.y, maskRadius, ripplePaint)
+ // To reduce overdraw, we mask the effect to a circle or a rectangle that's bigger than the
+ // 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
+ 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
+ canvas.drawRect(
+ /* left= */ centerX - maskWidth,
+ /* top= */ centerY - maskHeight,
+ /* right= */ centerX + maskWidth,
+ /* bottom= */ centerY + maskHeight,
+ ripplePaint)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt b/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
new file mode 100644
index 000000000000..4360105375cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ripple/SdfShaderLibrary.kt
@@ -0,0 +1,77 @@
+/*
+ * 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
+
+/** Library class that contains 2D signed distance functions. */
+class SdfShaderLibrary {
+ companion object {
+ const val CIRCLE_SDF = """
+ float sdCircle(vec2 p, float r) {
+ return (length(p)-r) / r;
+ }
+
+ float softCircle(vec2 p, vec2 origin, float radius, float blur) {
+ float d = sdCircle(p-origin, radius);
+ float blurHalf = blur * 0.5;
+ return smoothstep(-blurHalf, blurHalf, d);
+ }
+
+ float softRing(vec2 p, vec2 origin, float radius, float blur) {
+ float thicknessHalf = radius * 0.25;
+
+ float outerCircle = sdCircle(p-origin, radius + thicknessHalf);
+ float innerCircle = sdCircle(p-origin, radius);
+
+ float d = max(outerCircle, -innerCircle);
+ float blurHalf = blur * 0.5;
+
+ return smoothstep(-blurHalf, blurHalf, d);
+ }
+ """
+
+ const val ROUNDED_BOX_SDF = """
+ float sdRoundedBox(vec2 p, vec2 size, float cornerRadius) {
+ size *= 0.5;
+ cornerRadius *= 0.5;
+ vec2 d = abs(p)-size+cornerRadius;
+
+ float outside = length(max(d, 0.0));
+ float inside = min(max(d.x, d.y), 0.0);
+
+ return (outside+inside-cornerRadius)/size.y;
+ }
+
+ float softRoundedBox(vec2 p, vec2 origin, vec2 size, float cornerRadius, float blur) {
+ float d = sdRoundedBox(p-origin, size, cornerRadius);
+ float blurHalf = blur * 0.5;
+ return smoothstep(-blurHalf, blurHalf, d);
+ }
+
+ float softRoundedBoxRing(vec2 p, vec2 origin, vec2 size, float cornerRadius,
+ float borderThickness, float blur) {
+
+ float outerRoundBox = sdRoundedBox(p-origin, size, cornerRadius);
+ float innerRoundBox = sdRoundedBox(p-origin, size - vec2(borderThickness),
+ cornerRadius - borderThickness);
+
+ float d = max(outerRoundBox, -innerRoundBox);
+ float blurHalf = blur * 0.5;
+
+ return smoothstep(-blurHalf, blurHalf, d);
+ }
+ """
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
index 6978490a1252..3ac28c8d0d37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/charging/WiredChargingRippleControllerTest.kt
@@ -35,12 +35,12 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
-import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.eq
-import org.mockito.Mockito.reset
import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -63,6 +63,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
controller = WiredChargingRippleController(
commandRegistry, batteryController, configurationController,
featureFlags, context, windowManager, systemClock, uiEventLogger)
+ rippleView.setupShader()
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
controller.registerCallbacks()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
new file mode 100644
index 000000000000..f247c651cbca
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ripple/RippleViewTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+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
+ fun setup() {
+ rippleView = RippleView(context, null)
+ }
+
+ @Test
+ fun testSetupShader_compilesCircle() {
+ rippleView.setupShader(RippleShader.RippleShape.CIRCLE)
+ }
+
+ @Test
+ fun testSetupShader_compilesRoundedBox() {
+ rippleView.setupShader(RippleShader.RippleShape.ROUNDED_BOX)
+ }
+}