Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3574e111 authored by Yein Jo's avatar Yein Jo Committed by Android (Google) Code Review
Browse files

Merge "Add RippleRevealEffect" into main

parents ef12797b ab7c0fee
Loading
Loading
Loading
Loading
+111 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+65 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+144 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+50 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+91 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}