From fabc17ea4e0e7a0c571034ad4a63ed0b922a8894 Mon Sep 17 00:00:00 2001 From: Yein Jo Date: Wed, 15 Feb 2023 19:32:25 +0000 Subject: [PATCH] Support multiple sizes at specific progress t in RippleShader. Bug: 269124200 Test: adb shell cmd statusbar docking-ripple Test: RippleShaderTest Change-Id: Ic56314b48f96beac95a3fac02c1dabd8f630e042 --- .../surfaceeffects/ripple/RippleAnimation.kt | 2 +- .../surfaceeffects/ripple/RippleShader.kt | 132 +++++++++++++++--- .../surfaceeffects/ripple/RippleView.kt | 21 +-- .../systemui/biometrics/AuthRippleView.kt | 4 +- .../receiver/ReceiverChipRippleView.kt | 2 +- .../surfaceeffects/ripple/RippleShaderTest.kt | 121 ++++++++++++++++ 6 files changed, 250 insertions(+), 32 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleShaderTest.kt 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() + @VisibleForTesting var currentSizeIndex = 0 + @VisibleForTesting val initialSize = SizeAtProgress(0f, 0f, 0f) + + var currentWidth: Float = 0f + private set + var currentHeight: Float = 0f + private set + + /** + * Sets the max size of the ripple. + * + *

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