Support multiple sizes at specific progress t in RippleShader.
Bug: 269124200 Test: adb shell cmd statusbar docking-ripple Test: RippleShaderTest Change-Id: Ic56314b48f96beac95a3fac02c1dabd8f630e042
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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].
|
||||
*
|
||||
* <p>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)
|
||||
*
|
||||
* <p>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<SizeAtProgress>()
|
||||
@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.
|
||||
*
|
||||
* <p>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].
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
*
|
||||
* <p>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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user