diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..72f0e86f9dcea6652e9bd34d478d28fdbb9ef4d0 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxConfig.kt @@ -0,0 +1,46 @@ +/* + * 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.glowboxeffect + +/** Parameters used to play [GlowBoxEffect]. */ +data class GlowBoxConfig( + /** Start center position X in px. */ + val startCenterX: Float, + /** Start center position Y in px. */ + val startCenterY: Float, + /** End center position X in px. */ + val endCenterX: Float, + /** End center position Y in px. */ + val endCenterY: Float, + /** Width of the box in px. */ + val width: Float, + /** Height of the box in px. */ + val height: Float, + /** Color of the box in ARGB, Apply alpha value if needed. */ + val color: Int, + /** Amount of blur (or glow) of the box. */ + val blurAmount: Float, + /** + * Duration of the animation. Note that the full duration of the animation is + * [duration] + [easeInDuration] + [easeOutDuration]. + */ + val duration: Long, + /** Ease in duration of the animation. */ + val easeInDuration: Long, + /** Ease out duration of the animation. */ + val easeOutDuration: Long, +) diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a25cfd88e7342ffeb7e6ad143d8682188ffce16 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffect.kt @@ -0,0 +1,147 @@ +/* + * 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.glowboxeffect + +import android.animation.ValueAnimator +import android.graphics.Paint +import androidx.annotation.VisibleForTesting +import androidx.core.animation.doOnEnd +import com.android.systemui.surfaceeffects.PaintDrawCallback +import com.android.systemui.surfaceeffects.utils.MathUtils.lerp + +/** Glow box effect where the box moves from start to end positions defined in the [config]. */ +class GlowBoxEffect( + private val config: GlowBoxConfig, + private val paintDrawCallback: PaintDrawCallback +) { + private val glowBoxShader = + GlowBoxShader().apply { + setSize(config.width, config.height) + setCenter(config.startCenterX, config.startCenterY) + setBlur(config.blurAmount) + setColor(config.color) + } + private var animator: ValueAnimator? = null + @VisibleForTesting var state: AnimationState = AnimationState.NOT_PLAYING + private val paint = Paint().apply { shader = glowBoxShader } + + fun play() { + if (state != AnimationState.NOT_PLAYING) { + return + } + + playEaseIn() + } + + fun finish() { + if (state == AnimationState.NOT_PLAYING || state == AnimationState.EASE_OUT) { + return + } + + animator?.pause() + playEaseOut() + } + + private fun playEaseIn() { + if (state == AnimationState.EASE_IN) { + return + } + state = AnimationState.EASE_IN + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.easeInDuration + addUpdateListener { + val progress = it.animatedValue as Float + glowBoxShader.setCenter( + lerp(config.startCenterX, config.endCenterX, progress), + lerp(config.startCenterY, config.endCenterY, progress) + ) + + draw() + } + + doOnEnd { + animator = null + playMain() + } + + start() + } + } + + private fun playMain() { + if (state == AnimationState.MAIN) { + return + } + state = AnimationState.MAIN + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.duration + addUpdateListener { draw() } + + doOnEnd { + animator = null + playEaseOut() + } + + start() + } + } + + private fun playEaseOut() { + if (state == AnimationState.EASE_OUT) return + state = AnimationState.EASE_OUT + + animator = + ValueAnimator.ofFloat(0f, 1f).apply { + duration = config.easeOutDuration + addUpdateListener { + val progress = it.animatedValue as Float + glowBoxShader.setCenter( + lerp(config.endCenterX, config.startCenterX, progress), + lerp(config.endCenterY, config.startCenterY, progress) + ) + + draw() + } + + doOnEnd { + animator = null + state = AnimationState.NOT_PLAYING + } + + start() + } + } + + private fun draw() { + paintDrawCallback.onDraw(paint) + } + + /** + * The animation state of the effect. The animation state transitions as follows: [EASE_IN] -> + * [MAIN] -> [EASE_OUT] -> [NOT_PLAYING]. + */ + enum class AnimationState { + EASE_IN, + MAIN, + EASE_OUT, + NOT_PLAYING, + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt new file mode 100644 index 0000000000000000000000000000000000000000..36934086cc234bb11ae9213510213adf5bbebff5 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxShader.kt @@ -0,0 +1,58 @@ +/* + * 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.glowboxeffect + +import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaderutil.SdfShaderLibrary + +/** Soft box shader. */ +class GlowBoxShader : RuntimeShader(GLOW_SHADER) { + // language=AGSL + private companion object { + private const val SHADER = + """ + uniform half2 in_center; + uniform half2 in_size; + uniform half in_blur; + layout(color) uniform half4 in_color; + + float4 main(float2 fragcoord) { + half glow = soften(sdBox(fragcoord - in_center, in_size), in_blur); + return in_color * (1. - glow); + } + """ + + private const val GLOW_SHADER = + SdfShaderLibrary.BOX_SDF + SdfShaderLibrary.SHADER_SDF_OPERATION_LIB + SHADER + } + + fun setCenter(x: Float, y: Float) { + setFloatUniform("in_center", x, y) + } + + fun setSize(width: Float, height: Float) { + setFloatUniform("in_size", width, height) + } + + fun setBlur(blurAmount: Float) { + setFloatUniform("in_blur", blurAmount) + } + + fun setColor(color: Int) { + setColorUniform("in_color", color) + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt index 78898932249bb877bcde20192e8751202b78eb58..4efab58347dd7e8a4ff4b00422c857e97575a68e 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/shaderutil/SdfShaderLibrary.kt @@ -35,17 +35,26 @@ class SdfShaderLibrary { } """ + const val BOX_SDF = + """ + float sdBox(vec2 p, vec2 size) { + size = size * 0.5; + vec2 d = abs(p) - size; + return length(max(d, 0.)) + min(max(d.x, d.y), 0.) / size.y; + } + """ + 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; + 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; + return (outside + inside - cornerRadius) / size.y; } float roundedBoxRing(vec2 p, vec2 size, float cornerRadius, 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 0000000000000000000000000000000000000000..7ed3b87f684e8ff97cb5d0c517c1725d737c40fa --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/utils/MathUtils.kt @@ -0,0 +1,24 @@ +/* + * 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 lerp(start: Float, stop: Float, amount: Float): Float { + return start + (stop - start) * amount + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffectTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..16132ba7cff5acc469ed95c951afaf1767b6843f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/glowboxeffect/GlowBoxEffectTest.kt @@ -0,0 +1,124 @@ +/* + * 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.glowboxeffect + +import android.graphics.Color +import android.graphics.Paint +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.AnimatorTestRule +import com.android.systemui.surfaceeffects.PaintDrawCallback +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class GlowBoxEffectTest : SysuiTestCase() { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + private lateinit var config: GlowBoxConfig + private lateinit var glowBoxEffect: GlowBoxEffect + private lateinit var drawCallback: PaintDrawCallback + + @Before + fun setup() { + drawCallback = + object : PaintDrawCallback { + override fun onDraw(paint: Paint) {} + } + config = + GlowBoxConfig( + startCenterX = 0f, + startCenterY = 0f, + endCenterX = 0f, + endCenterY = 0f, + width = 1f, + height = 1f, + color = Color.WHITE, + blurAmount = 0.1f, + duration = 100L, + easeInDuration = 100L, + easeOutDuration = 100L + ) + glowBoxEffect = GlowBoxEffect(config, drawCallback) + } + + @Test + fun play_paintCallback_triggersDrawCallback() { + var paintFromCallback: Paint? = null + drawCallback = + object : PaintDrawCallback { + override fun onDraw(paint: Paint) { + paintFromCallback = paint + } + } + glowBoxEffect = GlowBoxEffect(config, drawCallback) + + assertThat(paintFromCallback).isNull() + + glowBoxEffect.play() + animatorTestRule.advanceTimeBy(50L) + + assertThat(paintFromCallback).isNotNull() + } + + @Test + fun play_followsAnimationStateInOrder() { + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.NOT_PLAYING) + + glowBoxEffect.play() + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.EASE_IN) + + animatorTestRule.advanceTimeBy(config.easeInDuration + 50L) + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.MAIN) + + animatorTestRule.advanceTimeBy(config.duration + 50L) + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.EASE_OUT) + + animatorTestRule.advanceTimeBy(config.easeOutDuration + 50L) + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.NOT_PLAYING) + } + + @Test + fun finish_statePlaying_finishesAnimation() { + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.NOT_PLAYING) + + glowBoxEffect.play() + glowBoxEffect.finish() + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.EASE_OUT) + } + + @Test + fun finish_stateNotPlaying_doesNotFinishAnimation() { + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.NOT_PLAYING) + + glowBoxEffect.finish() + + assertThat(glowBoxEffect.state).isEqualTo(GlowBoxEffect.AnimationState.NOT_PLAYING) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt index 6f589418cf1ef096e4f520f137ae3cbd66e84078..41d7fd54902dd39f1f75848875387ca443eabd69 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt @@ -21,8 +21,8 @@ import android.graphics.RenderEffect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase import com.android.systemui.animation.AnimatorTestRule -import com.android.systemui.model.SysUiStateTest import com.android.systemui.surfaceeffects.PaintDrawCallback import com.android.systemui.surfaceeffects.RenderEffectDrawCallback import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig @@ -35,7 +35,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -class LoadingEffectTest : SysUiStateTest() { +class LoadingEffectTest : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule(this)