diff options
| author | 2024-04-18 16:07:56 +0000 | |
|---|---|---|
| committer | 2024-04-18 16:07:56 +0000 | |
| commit | 3574e111caaf1c722f657a60e280c6d73377a6ac (patch) | |
| tree | cd0f2cbc69693a5d942007c89d345099c568c3f7 | |
| parent | ef12797b306bded977d4fa4381833a9eec638820 (diff) | |
| parent | ab7c0fee12d2efa0b5664e17508f340cdc359414 (diff) | |
Merge "Add RippleRevealEffect" into main
5 files changed, 461 insertions, 0 deletions
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffect.kt new file mode 100644 index 000000000000..ffa2b4662f33 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffect.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 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.revealeffect + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.graphics.RenderEffect +import androidx.core.graphics.ColorUtils +import com.android.systemui.surfaceeffects.RenderEffectDrawCallback +import com.android.systemui.surfaceeffects.utils.MathUtils +import kotlin.math.max +import kotlin.math.min + +/** Creates a reveal effect with a circular ripple sparkles on top. */ +class RippleRevealEffect( + private val config: RippleRevealEffectConfig, + private val renderEffectCallback: RenderEffectDrawCallback, + private val stateChangedCallback: AnimationStateChangedCallback? = null +) { + private val rippleRevealShader = RippleRevealShader().apply { applyConfig(config) } + private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f) + + fun play() { + if (animator.isRunning) { + return + } + + animator.duration = config.duration.toLong() + animator.addUpdateListener { updateListener -> + val playTime = updateListener.currentPlayTime.toFloat() + rippleRevealShader.setTime(playTime * TIME_SCALE_FACTOR) + + // Compute radius. + val progress = updateListener.animatedValue as Float + val innerRad = MathUtils.lerp(config.innerRadiusStart, config.innerRadiusEnd, progress) + val outerRad = MathUtils.lerp(config.outerRadiusStart, config.outerRadiusEnd, progress) + rippleRevealShader.setInnerRadius(innerRad) + rippleRevealShader.setOuterRadius(outerRad) + + // Compute alphas. + val innerAlphaProgress = + MathUtils.constrainedMap( + 1f, + 0f, + config.innerFadeOutStart, + config.duration, + playTime + ) + val outerAlphaProgress = + MathUtils.constrainedMap( + 1f, + 0f, + config.outerFadeOutStart, + config.duration, + playTime + ) + val innerAlpha = MathUtils.lerp(0f, 255f, innerAlphaProgress) + val outerAlpha = MathUtils.lerp(0f, 255f, outerAlphaProgress) + + val innerColor = ColorUtils.setAlphaComponent(config.innerColor, innerAlpha.toInt()) + val outerColor = ColorUtils.setAlphaComponent(config.outerColor, outerAlpha.toInt()) + rippleRevealShader.setInnerColor(innerColor) + rippleRevealShader.setOuterColor(outerColor) + + // Pass in progresses since those functions take in normalized alpha values. + rippleRevealShader.setBackgroundAlpha(max(innerAlphaProgress, outerAlphaProgress)) + rippleRevealShader.setSparkleAlpha(min(innerAlphaProgress, outerAlphaProgress)) + + // Trigger draw callback. + renderEffectCallback.onDraw( + RenderEffect.createRuntimeShaderEffect( + rippleRevealShader, + RippleRevealShader.BACKGROUND_UNIFORM + ) + ) + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + stateChangedCallback?.onAnimationEnd() + } + } + ) + animator.start() + stateChangedCallback?.onAnimationStart() + } + + interface AnimationStateChangedCallback { + fun onAnimationStart() + fun onAnimationEnd() + } + + private companion object { + private const val TIME_SCALE_FACTOR = 0.00175f + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectConfig.kt new file mode 100644 index 000000000000..9675f19613a8 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectConfig.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.revealeffect + +import android.graphics.Color + +/** Defines parameters needed for [RippleRevealEffect]. */ +data class RippleRevealEffectConfig( + /** Total duration of the animation. */ + val duration: Float = 0f, + /** Timestamp of when the inner mask starts fade out. (Linear fadeout) */ + val innerFadeOutStart: Float = 0f, + /** Timestamp of when the outer mask starts fade out. (Linear fadeout) */ + val outerFadeOutStart: Float = 0f, + /** Center x position of the effect. */ + val centerX: Float = 0f, + /** Center y position of the effect. */ + val centerY: Float = 0f, + /** Start radius of the inner circle. */ + val innerRadiusStart: Float = 0f, + /** End radius of the inner circle. */ + val innerRadiusEnd: Float = 0f, + /** Start radius of the outer circle. */ + val outerRadiusStart: Float = 0f, + /** End radius of the outer circle. */ + val outerRadiusEnd: Float = 0f, + /** + * Pixel density of the display. Do not pass a random value. The value must come from + * [context.resources.displayMetrics.density]. + */ + val pixelDensity: Float = 1f, + /** + * The amount the circle masks should be softened. Higher value will make the edge of the circle + * mask soft. + */ + val blurAmount: Float = 0f, + /** Color of the inner circle mask. */ + val innerColor: Int = Color.WHITE, + /** Color of the outer circle mask. */ + val outerColor: Int = Color.WHITE, + /** Multiplier to make the sparkles visible. */ + val sparkleStrength: Float = SPARKLE_STRENGTH, + /** Size of the sparkle. Expected range [0, 1]. */ + val sparkleScale: Float = SPARKLE_SCALE +) { + /** Default parameters. */ + companion object { + const val SPARKLE_STRENGTH: Float = 0.3f + const val SPARKLE_SCALE: Float = 0.8f + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealShader.kt new file mode 100644 index 000000000000..a3f979542055 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealShader.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 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.revealeffect + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary +import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary + +/** Circular reveal effect with sparkles. */ +class RippleRevealShader : RuntimeShader(SHADER) { + // language=AGSL + companion object { + const val BACKGROUND_UNIFORM = "in_dst" + private const val MAIN = + """ + uniform shader ${BACKGROUND_UNIFORM}; + uniform half in_dstAlpha; + uniform half in_time; + uniform vec2 in_center; + uniform half in_innerRadius; + uniform half in_outerRadius; + uniform half in_sparkleStrength; + uniform half in_blur; + uniform half in_pixelDensity; + uniform half in_sparkleScale; + uniform half in_sparkleAlpha; + layout(color) uniform vec4 in_innerColor; + layout(color) uniform vec4 in_outerColor; + + vec4 main(vec2 p) { + half innerMask = soften(sdCircle(p - in_center, in_innerRadius), in_blur); + half outerMask = soften(sdCircle(p - in_center, in_outerRadius), in_blur); + + // Flip it since we are interested in the circle. + innerMask = 1.-innerMask; + outerMask = 1.-outerMask; + + // Color two circles using the mask. + vec4 inColor = vec4(in_innerColor.rgb, 1.) * in_innerColor.a; + vec4 outColor = vec4(in_outerColor.rgb, 1.) * in_outerColor.a; + vec4 blend = mix(inColor, outColor, innerMask); + + vec4 dst = vec4(in_dst.eval(p).rgb, 1.); + dst *= in_dstAlpha; + + blend *= blend.a; + // Do normal blend with the background. + blend = blend + dst * (1. - blend.a); + + half sparkle = + sparkles(p - mod(p, in_pixelDensity * in_sparkleScale), in_time); + // Add sparkles using additive blending. + blend += sparkle * in_sparkleStrength * in_sparkleAlpha; + + // Mask everything at the end. + blend *= outerMask; + + return blend; + } + """ + + private const val SHADER = + ShaderUtilLibrary.SHADER_LIB + + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + + SdfShaderLibrary.CIRCLE_SDF + + MAIN + } + + fun applyConfig(config: RippleRevealEffectConfig) { + setCenter(config.centerX, config.centerY) + setInnerRadius(config.innerRadiusStart) + setOuterRadius(config.outerRadiusStart) + setBlurAmount(config.blurAmount) + setPixelDensity(config.pixelDensity) + setSparkleScale(config.sparkleScale) + setSparkleStrength(config.sparkleStrength) + setInnerColor(config.innerColor) + setOuterColor(config.outerColor) + } + + fun setTime(time: Float) { + setFloatUniform("in_time", time) + } + + fun setCenter(centerX: Float, centerY: Float) { + setFloatUniform("in_center", centerX, centerY) + } + + fun setInnerRadius(radius: Float) { + setFloatUniform("in_innerRadius", radius) + } + + fun setOuterRadius(radius: Float) { + setFloatUniform("in_outerRadius", radius) + } + + fun setBlurAmount(blurAmount: Float) { + setFloatUniform("in_blur", blurAmount) + } + + fun setPixelDensity(density: Float) { + setFloatUniform("in_pixelDensity", density) + } + + fun setSparkleScale(scale: Float) { + setFloatUniform("in_sparkleScale", scale) + } + + fun setSparkleStrength(strength: Float) { + setFloatUniform("in_sparkleStrength", strength) + } + + fun setInnerColor(color: Int) { + setColorUniform("in_innerColor", color) + } + + fun setOuterColor(color: Int) { + setColorUniform("in_outerColor", color) + } + + /** Sets the background alpha. Range [0,1]. */ + fun setBackgroundAlpha(alpha: Float) { + setFloatUniform("in_dstAlpha", alpha) + } + + /** Sets the sparkle alpha. Range [0,1]. */ + fun setSparkleAlpha(alpha: Float) { + setFloatUniform("in_sparkleAlpha", alpha) + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt new file mode 100644 index 000000000000..1411c32b813b --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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.utils + +/** Copied from android.utils.MathUtils */ +object MathUtils { + fun constrainedMap( + rangeMin: Float, + rangeMax: Float, + valueMin: Float, + valueMax: Float, + value: Float + ): Float { + return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value)) + } + + fun lerp(start: Float, stop: Float, amount: Float): Float { + return start + (stop - start) * amount + } + + fun lerpInv(a: Float, b: Float, value: Float): Float { + return if (a != b) (value - a) / (b - a) else 0.0f + } + + fun saturate(value: Float): Float { + return constrain(value, 0.0f, 1.0f) + } + + fun lerpInvSat(a: Float, b: Float, value: Float): Float { + return saturate(lerpInv(a, b, value)) + } + + fun constrain(amount: Float, low: Float, high: Float): Float { + return if (amount < low) low else if (amount > high) high else amount + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectTest.kt new file mode 100644 index 000000000000..b1df159cdefc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/revealeffect/RippleRevealEffectTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 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.revealeffect + +import android.graphics.RenderEffect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.animation.AnimatorTestRule +import com.android.systemui.model.SysUiStateTest +import com.android.systemui.surfaceeffects.RenderEffectDrawCallback +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class RippleRevealEffectTest : SysUiStateTest() { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + @Test + fun play_triggersDrawCallback() { + var effectFromCallback: RenderEffect? = null + val revealEffectConfig = RippleRevealEffectConfig(duration = 1000f) + val drawCallback = + object : RenderEffectDrawCallback { + override fun onDraw(renderEffect: RenderEffect) { + effectFromCallback = renderEffect + } + } + val revealEffect = RippleRevealEffect(revealEffectConfig, drawCallback) + assertThat(effectFromCallback).isNull() + + revealEffect.play() + + animatorTestRule.advanceTimeBy(500L) + + assertThat(effectFromCallback).isNotNull() + } + + @Test + fun play_triggersStateChangedCallback() { + val revealEffectConfig = RippleRevealEffectConfig(duration = 1000f) + val drawCallback = + object : RenderEffectDrawCallback { + override fun onDraw(renderEffect: RenderEffect) {} + } + var animationStartedCalled = false + var animationEndedCalled = false + val stateChangedCallback = + object : RippleRevealEffect.AnimationStateChangedCallback { + override fun onAnimationStart() { + animationStartedCalled = true + } + + override fun onAnimationEnd() { + animationEndedCalled = true + } + } + val revealEffect = + RippleRevealEffect(revealEffectConfig, drawCallback, stateChangedCallback) + + assertThat(animationStartedCalled).isFalse() + assertThat(animationEndedCalled).isFalse() + + revealEffect.play() + + assertThat(animationStartedCalled).isTrue() + + animatorTestRule.advanceTimeBy(revealEffectConfig.duration.toLong()) + + assertThat(animationEndedCalled).isTrue() + } +} |