diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt index 442c6fadb7c12..bd91c65ecc6e6 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt @@ -68,7 +68,7 @@ class RippleAnimation(private val config: RippleAnimationConfig) { private fun applyConfigToShader() { with(rippleShader) { setCenter(config.centerX, config.centerY) - setMaxSize(config.maxWidth, config.maxHeight) + rippleSize.setMaxSize(config.maxWidth, config.maxHeight) pixelDensity = config.pixelDensity color = ColorUtils.setAlphaComponent(config.color, config.opacity) sparkleStrength = config.sparkleStrength diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt index 3f19c0a341f53..aad2d70f57945 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt @@ -15,9 +15,10 @@ */ package com.android.systemui.surfaceeffects.ripple -import android.graphics.PointF import android.graphics.RuntimeShader +import android.util.Log import android.util.MathUtils +import androidx.annotation.VisibleForTesting import com.android.systemui.animation.Interpolators import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary @@ -44,6 +45,8 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : } // language=AGSL companion object { + private val TAG = RippleShader::class.simpleName + // Default fade in/ out values. The value range is [0,1]. const val DEFAULT_FADE_IN_START = 0f const val DEFAULT_FADE_OUT_END = 1f @@ -184,13 +187,6 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : setFloatUniform("in_center", x, y) } - /** Max width of the ripple. */ - private var maxSize: PointF = PointF() - fun setMaxSize(width: Float, height: Float) { - maxSize.x = width - maxSize.y = height - } - /** * Blur multipliers for the ripple. * @@ -200,6 +196,9 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : var blurStart: Float = 1.25f var blurEnd: Float = 0.5f + /** Size of the ripple. */ + val rippleSize = RippleSize() + /** * Linear progress of the ripple. Float value between [0, 1]. * @@ -220,13 +219,15 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : set(value) { field = value - currentWidth = maxSize.x * value - currentHeight = maxSize.y * value - setFloatUniform("in_size", currentWidth, currentHeight) + rippleSize.update(value) - setFloatUniform("in_thickness", maxSize.y * value * 0.5f) - // radius should not exceed width and height values. - setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * value) + setFloatUniform("in_size", rippleSize.currentWidth, rippleSize.currentHeight) + setFloatUniform("in_thickness", rippleSize.currentHeight * 0.5f) + // Corner radius is always max of the min between the current width and height. + setFloatUniform( + "in_cornerRadius", + Math.min(rippleSize.currentWidth, rippleSize.currentHeight) + ) setFloatUniform("in_blur", MathUtils.lerp(blurStart, blurEnd, value)) } @@ -275,12 +276,6 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : setFloatUniform("in_pixelDensity", value) } - var currentWidth: Float = 0f - private set - - var currentHeight: Float = 0f - private set - /** Parameters that are used to fade in/ out of the sparkle ring. */ val sparkleRingFadeParams = FadeParams( @@ -353,4 +348,101 @@ class RippleShader(rippleShape: RippleShape = RippleShape.CIRCLE) : /** The endpoint of the fade out, given that the animation goes from 0 to 1. */ var fadeOutEnd: Float = DEFAULT_FADE_OUT_END, ) + + /** + * Desired size of the ripple at a point t in [progress]. + * + *
Note that [progress] is curved and normalized. Below is an example usage: + * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 0.2f, width= 500f, height= + * 700f), SizeAtProgress(t= 1f, width= 100f, height= 300f) + * + *
For simple ripple effects, you will want to use [setMaxSize] as it is translated into:
+ * SizeAtProgress(t= 0f, width= 0f, height= 0f), SizeAtProgress(t= 1f, width= maxWidth, height=
+ * maxHeight)
+ */
+ data class SizeAtProgress(
+ /** Time t in [0,1] progress range. */
+ var t: Float,
+ /** Target width size of the ripple at time [t]. */
+ var width: Float,
+ /** Target height size of the ripple at time [t]. */
+ var height: Float
+ )
+
+ /** Updates and stores the ripple size. */
+ inner class RippleSize {
+ @VisibleForTesting var sizes = mutableListOf Use this if the ripple shape simply changes linearly.
+ */
+ fun setMaxSize(width: Float, height: Float) {
+ setSizeAtProgresses(initialSize, SizeAtProgress(1f, width, height))
+ }
+
+ /**
+ * Sets the list of [sizes].
+ *
+ * Note that setting this clears the existing sizes.
+ */
+ fun setSizeAtProgresses(vararg sizes: SizeAtProgress) {
+ // Reset everything.
+ this.sizes.clear()
+ currentSizeIndex = 0
+
+ this.sizes.addAll(sizes)
+ this.sizes.sortBy { it.t }
+ }
+
+ /**
+ * Updates the current ripple size based on the progress.
+ *
+ * Should be called when progress updates.
+ */
+ fun update(progress: Float) {
+ val targetIndex = updateTargetIndex(progress)
+ val prevIndex = Math.max(targetIndex - 1, 0)
+
+ val targetSize = sizes[targetIndex]
+ val prevSize = sizes[prevIndex]
+
+ val subProgress = subProgress(prevSize.t, targetSize.t, progress)
+
+ currentWidth = targetSize.width * subProgress + prevSize.width
+ currentHeight = targetSize.height * subProgress + prevSize.height
+ }
+
+ private fun updateTargetIndex(progress: Float): Int {
+ if (sizes.isEmpty()) {
+ // It could be empty on init.
+ if (progress > 0f) {
+ Log.e(
+ TAG,
+ "Did you forget to set the ripple size? Use [setMaxSize] or " +
+ "[setSizeAtProgresses] before playing the animation."
+ )
+ }
+ // If there's no size is set, we set everything to 0.
+ setSizeAtProgresses(initialSize)
+ }
+
+ var candidate = sizes[currentSizeIndex]
+
+ while (progress > candidate.t) {
+ currentSizeIndex = Math.min(currentSizeIndex + 1, sizes.size - 1)
+ candidate = sizes[currentSizeIndex]
+ }
+
+ return currentSizeIndex
+ }
+ }
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index 401c24aab5bff..ef5663fdcf6cb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -45,12 +45,8 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
var duration: Long = 1750
- 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)
+ rippleShader.rippleSize.setMaxSize(maxWidth, maxHeight)
}
private var centerX: Float = 0.0f
@@ -94,6 +90,15 @@ open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, a
rippleShader.blurEnd = end
}
+ /**
+ * Sets the list of [RippleShader.SizeAtProgress].
+ *
+ * Note that this clears the list before it sets with the new data.
+ */
+ fun setSizeAtProgresses(vararg targetSizes: RippleShader.SizeAtProgress) {
+ rippleShader.rippleSize.setSizeAtProgresses(*targetSizes)
+ }
+
@JvmOverloads
fun startRipple(onAnimationEnd: Runnable? = null) {
if (animator.isRunning) {
@@ -145,11 +150,11 @@ 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. (Twice bigger)
if (rippleShape == RippleShape.CIRCLE) {
- val maskRadius = rippleShader.currentWidth
+ val maskRadius = rippleShader.rippleSize.currentWidth
canvas.drawCircle(centerX, centerY, maskRadius, ripplePaint)
} else {
- val maskWidth = rippleShader.currentWidth * 2
- val maskHeight = rippleShader.currentHeight * 2
+ val maskWidth = rippleShader.rippleSize.currentWidth * 2
+ val maskHeight = rippleShader.rippleSize.currentHeight * 2
canvas.drawRect(
/* left= */ centerX - maskWidth,
/* top= */ centerY - maskHeight,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
index 58b230f52e93e..4b32759588e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt
@@ -75,7 +75,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
}
private var radius: Float = 0f
set(value) {
- rippleShader.setMaxSize(value * 2f, value * 2f)
+ rippleShader.rippleSize.setMaxSize(value * 2f, value * 2f)
field = value
}
private var origin: Point = Point()
@@ -364,7 +364,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
if (drawRipple) {
canvas?.drawCircle(origin.x.toFloat(), origin.y.toFloat(),
- rippleShader.currentWidth, ripplePaint)
+ rippleShader.rippleSize.currentWidth, ripplePaint)
}
}
}
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 4ff082ad6e06b..0b0535df6228c 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
@@ -98,7 +98,7 @@ class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleVi
// Calculates the actual starting percentage according to ripple shader progress set method.
// Check calculations in [RippleShader.progress]
fun calculateStartingPercentage(newHeight: Float): Float {
- val ratio = rippleShader.currentHeight / newHeight
+ val ratio = rippleShader.rippleSize.currentHeight / newHeight
val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
return 1 - remainingPercentage
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt
new file mode 100644
index 0000000000000..89cc18cc5d67b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 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.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RippleShaderTest : SysuiTestCase() {
+
+ private lateinit var rippleShader: RippleShader
+
+ @Before
+ fun setup() {
+ rippleShader = RippleShader()
+ }
+
+ @Test
+ fun setMaxSize_hasCorrectSizes() {
+ val expectedMaxWidth = 300f
+ val expectedMaxHeight = 500f
+
+ rippleShader.rippleSize.setMaxSize(expectedMaxWidth, expectedMaxHeight)
+
+ assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(2)
+ assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(rippleShader.rippleSize.initialSize)
+ val maxSize = rippleShader.rippleSize.sizes[1]
+ assertThat(maxSize.t).isEqualTo(1f)
+ assertThat(maxSize.width).isEqualTo(expectedMaxWidth)
+ assertThat(maxSize.height).isEqualTo(expectedMaxHeight)
+ }
+
+ @Test
+ fun setSizeAtProgresses_hasCorrectSizes() {
+ val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+ val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+ val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+
+ rippleShader.rippleSize.setSizeAtProgresses(expectedSize0, expectedSize1, expectedSize2)
+
+ assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(3)
+ assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(expectedSize0)
+ assertThat(rippleShader.rippleSize.sizes[1]).isEqualTo(expectedSize1)
+ assertThat(rippleShader.rippleSize.sizes[2]).isEqualTo(expectedSize2)
+ }
+
+ @Test
+ fun setSizeAtProgresses_sizeListIsSortedByT() {
+ val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+ val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+ val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+ val expectedSize3 = RippleShader.SizeAtProgress(t = 0.8f, width = 300f, height = 900f)
+ val expectedSize4 = RippleShader.SizeAtProgress(t = 1f, width = 500f, height = 300f)
+
+ // Add them in unsorted order
+ rippleShader.rippleSize.setSizeAtProgresses(
+ expectedSize0,
+ expectedSize3,
+ expectedSize2,
+ expectedSize4,
+ expectedSize1
+ )
+
+ assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(5)
+ assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(expectedSize0)
+ assertThat(rippleShader.rippleSize.sizes[1]).isEqualTo(expectedSize1)
+ assertThat(rippleShader.rippleSize.sizes[2]).isEqualTo(expectedSize2)
+ assertThat(rippleShader.rippleSize.sizes[3]).isEqualTo(expectedSize3)
+ assertThat(rippleShader.rippleSize.sizes[4]).isEqualTo(expectedSize4)
+ }
+
+ @Test
+ fun update_getsCorrectNextTargetSize() {
+ val expectedSize0 = RippleShader.SizeAtProgress(t = 0f, width = 100f, height = 100f)
+ val expectedSize1 = RippleShader.SizeAtProgress(t = 0.2f, width = 1500f, height = 1200f)
+ val expectedSize2 = RippleShader.SizeAtProgress(t = 0.4f, width = 200f, height = 70f)
+ val expectedSize3 = RippleShader.SizeAtProgress(t = 0.8f, width = 300f, height = 900f)
+ val expectedSize4 = RippleShader.SizeAtProgress(t = 1f, width = 500f, height = 300f)
+
+ rippleShader.rippleSize.setSizeAtProgresses(
+ expectedSize0,
+ expectedSize1,
+ expectedSize2,
+ expectedSize3,
+ expectedSize4
+ )
+
+ rippleShader.rippleSize.update(0.5f)
+ // Progress is between 0.4 and 0.8 (expectedSize3 and 4), so the index should be 3.
+ assertThat(rippleShader.rippleSize.currentSizeIndex).isEqualTo(3)
+ }
+
+ @Test
+ fun update_sizeListIsEmpty_setsInitialSize() {
+ assertThat(rippleShader.rippleSize.sizes).isEmpty()
+
+ rippleShader.rippleSize.update(0.3f)
+
+ assertThat(rippleShader.rippleSize.sizes.size).isEqualTo(1)
+ assertThat(rippleShader.rippleSize.sizes[0]).isEqualTo(rippleShader.rippleSize.initialSize)
+ }
+}