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

Commit 1887b976 authored by Yein Jo's avatar Yein Jo
Browse files

Refactor Turbulence noise

1. introduced three states (ease-in, main, and ease-out) to better control each animation & support non-fixed duration animation
(such as loading).
2. move business logic to the controller.

Follow up tasks (ordered in priority):
- have media player send a signal to end the animation. (currently it
  finishes after the max duration.)
- update animation curve (currently it's all linear)
- support force finish if needed (currently finish only works when the
  state is MAIN)

Bug: 237282226
Test: TurbulenceNoiseViewTest, TurbulenceNoiseControllerTest
Change-Id: I711c87096c7c9af6e322104ad0138c4fd78c4953
parent 971c4790
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -49,13 +49,16 @@ data class TurbulenceNoiseAnimationConfig(
    val opacity: Int = DEFAULT_OPACITY,
    val width: Float = 0f,
    val height: Float = 0f,
    val duration: Float = DEFAULT_NOISE_DURATION_IN_MILLIS,
    val maxDuration: Float = DEFAULT_MAX_DURATION_IN_MILLIS,
    val easeInDuration: Float = DEFAULT_EASING_DURATION_IN_MILLIS,
    val easeOutDuration: Float = DEFAULT_EASING_DURATION_IN_MILLIS,
    val pixelDensity: Float = 1f,
    val blendMode: BlendMode = DEFAULT_BLEND_MODE,
    val onAnimationEnd: Runnable? = null
) {
    companion object {
        const val DEFAULT_NOISE_DURATION_IN_MILLIS = 7500F
        const val DEFAULT_MAX_DURATION_IN_MILLIS = 7500f
        const val DEFAULT_EASING_DURATION_IN_MILLIS = 750f
        const val DEFAULT_LUMINOSITY_MULTIPLIER = 1f
        const val DEFAULT_NOISE_GRID_COUNT = 1.2f
        const val DEFAULT_NOISE_SPEED_Z = 0.3f
+95 −5
Original line number Diff line number Diff line
@@ -15,16 +15,106 @@
 */
package com.android.systemui.surfaceeffects.turbulencenoise

/** A controller that plays [TurbulenceNoiseView]. */
import android.view.View
import androidx.annotation.VisibleForTesting
import java.util.Random

/** Plays [TurbulenceNoiseView] in ease-in, main (no easing), and ease-out order. */
class TurbulenceNoiseController(private val turbulenceNoiseView: TurbulenceNoiseView) {

    companion object {
        /**
         * States of the turbulence noise animation.
         *
         * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN],
         * [AnimationState.MAIN], [AnimationState.EASE_OUT].
         */
        enum class AnimationState {
            EASE_IN,
            MAIN,
            EASE_OUT,
            NOT_PLAYING
        }
    }

    private val random = Random()

    /** Current state of the animation. */
    @VisibleForTesting
    var state: AnimationState = AnimationState.NOT_PLAYING
        set(value) {
            field = value
            if (state == AnimationState.NOT_PLAYING) {
                turbulenceNoiseView.visibility = View.INVISIBLE
                turbulenceNoiseView.clearConfig()
            } else {
                turbulenceNoiseView.visibility = View.VISIBLE
            }
        }

    init {
        turbulenceNoiseView.visibility = View.INVISIBLE
    }

    /** Updates the color of the noise. */
    fun updateNoiseColor(color: Int) {
        if (state == AnimationState.NOT_PLAYING) {
            return
        }
        turbulenceNoiseView.updateColor(color)
    }

    // TODO: add cancel and/ or pause once design requirements become clear.
    /** Plays [TurbulenceNoiseView] with the given config. */
    fun play(turbulenceNoiseAnimationConfig: TurbulenceNoiseAnimationConfig) {
        turbulenceNoiseView.play(turbulenceNoiseAnimationConfig)
    /**
     * Plays [TurbulenceNoiseView] with the given config.
     *
     * <p>It plays ease-in, main, and ease-out animations in sequence.
     */
    fun play(config: TurbulenceNoiseAnimationConfig) {
        if (state != AnimationState.NOT_PLAYING) {
            return // Ignore if any of the animation is playing.
        }

        turbulenceNoiseView.applyConfig(config)
        playEaseInAnimation()
    }

    // TODO(b/237282226): Support force finish.
    /** Finishes the main animation, which triggers the ease-out animation. */
    fun finish() {
        if (state == AnimationState.MAIN) {
            turbulenceNoiseView.finish(nextAnimation = this::playEaseOutAnimation)
        }
    }

    private fun playEaseInAnimation() {
        if (state != AnimationState.NOT_PLAYING) {
            return
        }
        state = AnimationState.EASE_IN

        // Add offset to avoid repetitive noise.
        turbulenceNoiseView.playEaseIn(
            offsetX = random.nextFloat(),
            offsetY = random.nextFloat(),
            this::playMainAnimation
        )
    }

    private fun playMainAnimation() {
        if (state != AnimationState.EASE_IN) {
            return
        }
        state = AnimationState.MAIN

        turbulenceNoiseView.play(this::playEaseOutAnimation)
    }

    private fun playEaseOutAnimation() {
        if (state != AnimationState.MAIN) {
            return
        }
        state = AnimationState.EASE_OUT

        turbulenceNoiseView.playEaseOut(onAnimationEnd = { state = AnimationState.NOT_PLAYING })
    }
}
+13 −2
Original line number Diff line number Diff line
@@ -114,8 +114,19 @@ class TurbulenceNoiseShader : RuntimeShader(TURBULENCE_NOISE_SHADER) {
        setFloatUniform("in_aspectRatio", width / max(height, 0.001f))
    }

    /** Sets noise move speed in x, y, and z direction. */
    /** Current noise movements in x, y, and z axes. */
    var noiseOffsetX: Float = 0f
        private set
    var noiseOffsetY: Float = 0f
        private set
    var noiseOffsetZ: Float = 0f
        private set

    /** Sets noise move offset in x, y, and z direction. */
    fun setNoiseMove(x: Float, y: Float, z: Float) {
        setFloatUniform("in_noiseMove", x, y, z)
        noiseOffsetX = x
        noiseOffsetY = y
        noiseOffsetZ = z
        setFloatUniform("in_noiseMove", noiseOffsetX, noiseOffsetY, noiseOffsetZ)
    }
}
+149 −43
Original line number Diff line number Diff line
@@ -25,75 +25,162 @@ import android.util.AttributeSet
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.core.graphics.ColorUtils
import java.util.Random
import kotlin.math.sin

/** View that renders turbulence noise effect. */
/**
 * View that renders turbulence noise effect.
 *
 * <p>Use [TurbulenceNoiseController] to control the turbulence animation. If you want to make some
 * other turbulence noise effects, either add functionality to [TurbulenceNoiseController] or create
 * another controller instead of extend or modify the [TurbulenceNoiseView].
 *
 * <p>Please keep the [TurbulenceNoiseView] (or View in general) not aware of the state.
 *
 * <p>Please avoid inheriting the View if possible. Instead, reconsider adding a controller for a
 * new case.
 */
class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    companion object {
        private const val MS_TO_SEC = 0.001f
        private const val TWO_PI = Math.PI.toFloat() * 2f
    }

    @VisibleForTesting val turbulenceNoiseShader = TurbulenceNoiseShader()
    private val turbulenceNoiseShader = TurbulenceNoiseShader()
    private val paint = Paint().apply { this.shader = turbulenceNoiseShader }
    private val random = Random()
    private val animator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f)
    private var config: TurbulenceNoiseAnimationConfig? = null
    @VisibleForTesting var noiseConfig: TurbulenceNoiseAnimationConfig? = null
    @VisibleForTesting var currentAnimator: ValueAnimator? = null

    val isPlaying: Boolean
        get() = animator.isRunning
    override fun onDraw(canvas: Canvas?) {
        if (canvas == null || !canvas.isHardwareAccelerated) {
            // Drawing with the turbulence noise shader requires hardware acceleration, so skip
            // if it's unsupported.
            return
        }

    init {
        // Only visible during the animation.
        visibility = INVISIBLE
        canvas.drawPaint(paint)
    }

    /** Updates the color during the animation. No-op if there's no animation playing. */
    fun updateColor(color: Int) {
        config?.let {
            it.color = color
            applyConfig(it)
    internal fun updateColor(color: Int) {
        noiseConfig?.let {
            turbulenceNoiseShader.setColor(ColorUtils.setAlphaComponent(color, it.opacity))
        }
    }

    override fun onDraw(canvas: Canvas?) {
        if (canvas == null || !canvas.isHardwareAccelerated) {
            // Drawing with the turbulence noise shader requires hardware acceleration, so skip
            // if it's unsupported.
    /** Plays the turbulence noise with no easing. */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    fun play(onAnimationEnd: Runnable? = null) {
        if (noiseConfig == null) {
            return
        }
        val config = noiseConfig!!

        canvas.drawPaint(paint)
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = config.maxDuration.toLong()

        // Animation should start from the initial position to avoid abrupt transition.
        val initialX = turbulenceNoiseShader.noiseOffsetX
        val initialY = turbulenceNoiseShader.noiseOffsetY
        val initialZ = turbulenceNoiseShader.noiseOffsetZ

        animator.addUpdateListener { updateListener ->
            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
            turbulenceNoiseShader.setNoiseMove(
                initialX + timeInSec * config.noiseMoveSpeedX,
                initialY + timeInSec * config.noiseMoveSpeedY,
                initialZ + timeInSec * config.noiseMoveSpeedZ
            )

            turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)

            invalidate()
        }

        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    currentAnimator = null
                    onAnimationEnd?.run()
                }
            }
        )

        animator.start()
        currentAnimator = animator
    }

    /** Plays the turbulence noise with linear ease-in. */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    fun playEaseIn(offsetX: Float = 0f, offsetY: Float = 0f, onAnimationEnd: Runnable? = null) {
        if (noiseConfig == null) {
            return
        }
        val config = noiseConfig!!

        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = config.easeInDuration.toLong()

        // Animation should start from the initial position to avoid abrupt transition.
        val initialX = turbulenceNoiseShader.noiseOffsetX
        val initialY = turbulenceNoiseShader.noiseOffsetY
        val initialZ = turbulenceNoiseShader.noiseOffsetZ

        animator.addUpdateListener { updateListener ->
            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
            val progress = updateListener.animatedValue as Float

            turbulenceNoiseShader.setNoiseMove(
                offsetX + initialX + timeInSec * config.noiseMoveSpeedX,
                offsetY + initialY + timeInSec * config.noiseMoveSpeedY,
                initialZ + timeInSec * config.noiseMoveSpeedZ
            )

            // TODO: Replace it with a better curve.
            turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)

            invalidate()
        }

        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    currentAnimator = null
                    onAnimationEnd?.run()
                }
            }
        )

    fun play(config: TurbulenceNoiseAnimationConfig) {
        if (isPlaying) {
            return // Ignore if the animation is playing.
        animator.start()
        currentAnimator = animator
    }
        visibility = VISIBLE
        applyConfig(config)

        // Add random offset to avoid same patterned noise.
        val offsetX = random.nextFloat()
        val offsetY = random.nextFloat()
    /** Plays the turbulence noise with linear ease-out. */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    fun playEaseOut(onAnimationEnd: Runnable? = null) {
        if (noiseConfig == null) {
            return
        }
        val config = noiseConfig!!

        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = config.easeOutDuration.toLong()

        // Animation should start from the initial position to avoid abrupt transition.
        val initialX = turbulenceNoiseShader.noiseOffsetX
        val initialY = turbulenceNoiseShader.noiseOffsetY
        val initialZ = turbulenceNoiseShader.noiseOffsetZ

        animator.duration = config.duration.toLong()
        animator.addUpdateListener { updateListener ->
            val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
            // Remap [0,1] to [0, 2*PI]
            val progress = TWO_PI * updateListener.animatedValue as Float
            val progress = updateListener.animatedValue as Float

            turbulenceNoiseShader.setNoiseMove(
                offsetX + timeInSec * config.noiseMoveSpeedX,
                offsetY + timeInSec * config.noiseMoveSpeedY,
                timeInSec * config.noiseMoveSpeedZ
                initialX + timeInSec * config.noiseMoveSpeedX,
                initialY + timeInSec * config.noiseMoveSpeedY,
                initialZ + timeInSec * config.noiseMoveSpeedZ
            )

            // Fade in and out the noise as the animation progress.
            // TODO: replace it with a better curve
            turbulenceNoiseShader.setOpacity(sin(TWO_PI - progress) * config.luminosityMultiplier)
            // TODO: Replace it with a better curve.
            turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)

            invalidate()
        }
@@ -101,16 +188,31 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex
        animator.addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    visibility = INVISIBLE
                    config.onAnimationEnd?.run()
                    currentAnimator = null
                    onAnimationEnd?.run()
                }
            }
        )

        animator.start()
        currentAnimator = animator
    }

    /** Finishes the current animation if playing and plays the next animation if given. */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    fun finish(nextAnimation: Runnable? = null) {
        // Calling Animator#end sets the animation state back to the initial state. Using pause to
        // avoid visual artifacts.
        currentAnimator?.pause()
        currentAnimator = null

        nextAnimation?.run()
    }

    private fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
        this.config = config
    /** Applies shader uniforms. Must be called before playing animation. */
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
        noiseConfig = config
        with(turbulenceNoiseShader) {
            setGridCount(config.gridCount)
            setColor(ColorUtils.setAlphaComponent(config.color, config.opacity))
@@ -120,4 +222,8 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex
        }
        paint.blendMode = config.blendMode
    }

    internal fun clearConfig() {
        noiseConfig = null
    }
}
+3 −1
Original line number Diff line number Diff line
@@ -1070,7 +1070,9 @@ public class MediaControlPanel {
                TurbulenceNoiseAnimationConfig.DEFAULT_OPACITY,
                /* width= */ mMediaViewHolder.getMultiRippleView().getWidth(),
                /* height= */ mMediaViewHolder.getMultiRippleView().getHeight(),
                TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_DURATION_IN_MILLIS,
                TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
                TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
                TurbulenceNoiseAnimationConfig.DEFAULT_EASING_DURATION_IN_MILLIS,
                this.getContext().getResources().getDisplayMetrics().density,
                BlendMode.PLUS,
                /* onAnimationEnd= */ null
Loading