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
This commit is contained in:
Yein Jo
2022-12-15 00:41:33 +00:00
parent 971c479068
commit 1887b97669
7 changed files with 420 additions and 100 deletions

View File

@@ -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

View File

@@ -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 })
}
}

View File

@@ -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)
}
}

View File

@@ -25,38 +25,29 @@ 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
val isPlaying: Boolean
get() = animator.isRunning
init {
// Only visible during the animation.
visibility = INVISIBLE
}
/** 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)
}
}
@VisibleForTesting var noiseConfig: TurbulenceNoiseAnimationConfig? = null
@VisibleForTesting var currentAnimator: ValueAnimator? = null
override fun onDraw(canvas: Canvas?) {
if (canvas == null || !canvas.isHardwareAccelerated) {
@@ -68,32 +59,38 @@ class TurbulenceNoiseView(context: Context?, attrs: AttributeSet?) : View(contex
canvas.drawPaint(paint)
}
fun play(config: TurbulenceNoiseAnimationConfig) {
if (isPlaying) {
return // Ignore if the animation is playing.
/** Updates the color during the animation. No-op if there's no animation playing. */
internal fun updateColor(color: Int) {
noiseConfig?.let {
turbulenceNoiseShader.setColor(ColorUtils.setAlphaComponent(color, it.opacity))
}
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 no easing. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
fun play(onAnimationEnd: Runnable? = null) {
if (noiseConfig == null) {
return
}
val config = noiseConfig!!
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.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
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)
turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
invalidate()
}
@@ -101,16 +98,121 @@ 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
}
private fun applyConfig(config: TurbulenceNoiseAnimationConfig) {
this.config = config
/** 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()
}
}
)
animator.start()
currentAnimator = animator
}
/** 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.addUpdateListener { updateListener ->
val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
val progress = updateListener.animatedValue as Float
turbulenceNoiseShader.setNoiseMove(
initialX + timeInSec * config.noiseMoveSpeedX,
initialY + timeInSec * config.noiseMoveSpeedY,
initialZ + timeInSec * config.noiseMoveSpeedZ
)
// TODO: Replace it with a better curve.
turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
invalidate()
}
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
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()
}
/** 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
}
}

View File

@@ -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

View File

@@ -17,8 +17,14 @@ package com.android.systemui.surfaceeffects.turbulencenoise
import android.graphics.Color
import android.testing.AndroidTestingRunner
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController.Companion.AnimationState.EASE_IN
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController.Companion.AnimationState.EASE_OUT
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController.Companion.AnimationState.MAIN
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController.Companion.AnimationState.NOT_PLAYING
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -33,26 +39,117 @@ class TurbulenceNoiseControllerTest : SysuiTestCase() {
private val fakeExecutor = FakeExecutor(fakeSystemClock)
@Test
fun play_playsTurbulenceNoise() {
val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
fun play_playsTurbulenceNoiseInOrder() {
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
assertThat(turbulenceNoiseController.state).isEqualTo(NOT_PLAYING)
fakeExecutor.execute {
turbulenceNoiseController.play(config)
assertThat(turbulenceNoiseView.isPlaying).isTrue()
assertThat(turbulenceNoiseController.state).isEqualTo(EASE_IN)
fakeSystemClock.advanceTime(config.duration.toLong())
fakeSystemClock.advanceTime(config.easeInDuration.toLong())
assertThat(turbulenceNoiseView.isPlaying).isFalse()
assertThat(turbulenceNoiseController.state).isEqualTo(MAIN)
fakeSystemClock.advanceTime(config.maxDuration.toLong())
assertThat(turbulenceNoiseController.state).isEqualTo(EASE_OUT)
fakeSystemClock.advanceTime(config.easeOutDuration.toLong())
assertThat(turbulenceNoiseController.state).isEqualTo(NOT_PLAYING)
}
}
@Test
fun play_alreadyPlaying_ignoresNewAnimationRequest() {
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
// Currently playing the main animation.
val turbulenceNoiseController =
TurbulenceNoiseController(turbulenceNoiseView).also { it.state = MAIN }
fakeExecutor.execute {
// Request another animation
turbulenceNoiseController.play(config)
assertThat(turbulenceNoiseController.state).isEqualTo(MAIN)
}
}
@Test
fun finish_mainAnimationPlaying_playsEaseOutAnimation() {
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val turbulenceNoiseController =
TurbulenceNoiseController(turbulenceNoiseView).also { it.state = MAIN }
fakeExecutor.execute {
turbulenceNoiseController.play(config)
fakeSystemClock.advanceTime(config.maxDuration.toLong() / 2)
turbulenceNoiseController.finish()
assertThat(turbulenceNoiseController.state).isEqualTo(EASE_OUT)
}
}
@Test
fun finish_nonMainAnimationPlaying_doesNotFinishAnimation() {
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val turbulenceNoiseController =
TurbulenceNoiseController(turbulenceNoiseView).also { it.state = EASE_IN }
fakeExecutor.execute {
turbulenceNoiseController.play(config)
fakeSystemClock.advanceTime(config.maxDuration.toLong() / 2)
turbulenceNoiseController.finish()
assertThat(turbulenceNoiseController.state).isEqualTo(EASE_IN)
}
}
@Test
fun onAnimationFinished_resetsStateCorrectly() {
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val turbulenceNoiseController = TurbulenceNoiseController(turbulenceNoiseView)
assertThat(turbulenceNoiseController.state).isEqualTo(NOT_PLAYING)
assertThat(turbulenceNoiseView.visibility).isEqualTo(INVISIBLE)
assertThat(turbulenceNoiseView.noiseConfig).isNull()
fakeExecutor.execute {
turbulenceNoiseController.play(config)
assertThat(turbulenceNoiseController.state).isEqualTo(EASE_IN)
assertThat(turbulenceNoiseView.visibility).isEqualTo(VISIBLE)
assertThat(turbulenceNoiseView.noiseConfig).isEqualTo(config)
// Play all the animations.
fakeSystemClock.advanceTime(
config.easeInDuration.toLong() +
config.maxDuration.toLong() +
config.easeOutDuration.toLong()
)
assertThat(turbulenceNoiseController.state).isEqualTo(NOT_PLAYING)
assertThat(turbulenceNoiseView.visibility).isEqualTo(INVISIBLE)
assertThat(turbulenceNoiseView.noiseConfig).isNull()
}
}
@Test
fun updateColor_updatesCorrectColor() {
val config = TurbulenceNoiseAnimationConfig(duration = 1000f, color = Color.WHITE)
val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f, color = Color.WHITE)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val expectedColor = Color.RED
@@ -61,9 +158,9 @@ class TurbulenceNoiseControllerTest : SysuiTestCase() {
fakeExecutor.execute {
turbulenceNoiseController.play(config)
turbulenceNoiseView.updateColor(expectedColor)
turbulenceNoiseController.updateNoiseColor(expectedColor)
fakeSystemClock.advanceTime(config.duration.toLong())
fakeSystemClock.advanceTime(config.maxDuration.toLong())
assertThat(config.color).isEqualTo(expectedColor)
}

View File

@@ -16,7 +16,6 @@
package com.android.systemui.surfaceeffects.turbulencenoise
import android.testing.AndroidTestingRunner
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor
@@ -33,54 +32,66 @@ class TurbulenceNoiseViewTest : SysuiTestCase() {
// FakeExecutor is needed to run animator.
private val fakeExecutor = FakeExecutor(fakeSystemClock)
@Test
fun play_viewHasCorrectVisibility() {
val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
fakeExecutor.execute {
turbulenceNoiseView.play(config)
assertThat(turbulenceNoiseView.visibility).isEqualTo(View.VISIBLE)
fakeSystemClock.advanceTime(config.duration.toLong())
assertThat(turbulenceNoiseView.visibility).isEqualTo(View.INVISIBLE)
}
}
@Test
fun play_playsAnimation() {
val config = TurbulenceNoiseAnimationConfig(duration = 1000f)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
val config = TurbulenceNoiseAnimationConfig()
val turbulenceNoiseView = TurbulenceNoiseView(context, null).also { it.applyConfig(config) }
var onAnimationEndCalled = false
fakeExecutor.execute {
turbulenceNoiseView.play(config)
turbulenceNoiseView.play(onAnimationEnd = { onAnimationEndCalled = true })
assertThat(turbulenceNoiseView.isPlaying).isTrue()
fakeSystemClock.advanceTime(config.maxDuration.toLong())
assertThat(onAnimationEndCalled).isTrue()
}
}
@Test
fun play_onEnd_triggersOnAnimationEnd() {
var animationEnd = false
val config =
TurbulenceNoiseAnimationConfig(
duration = 1000f,
onAnimationEnd = { animationEnd = true }
)
val turbulenceNoiseView = TurbulenceNoiseView(context, null)
fun playEaseIn_playsEaseInAnimation() {
val config = TurbulenceNoiseAnimationConfig()
val turbulenceNoiseView = TurbulenceNoiseView(context, null).also { it.applyConfig(config) }
var onAnimationEndCalled = false
fakeExecutor.execute {
turbulenceNoiseView.play(config)
turbulenceNoiseView.playEaseIn(onAnimationEnd = { onAnimationEndCalled = true })
assertThat(turbulenceNoiseView.isPlaying).isTrue()
fakeSystemClock.advanceTime(config.easeInDuration.toLong())
fakeSystemClock.advanceTime(config.duration.toLong())
assertThat(onAnimationEndCalled).isTrue()
}
}
assertThat(animationEnd).isTrue()
@Test
fun playEaseOut_playsEaseOutAnimation() {
val config = TurbulenceNoiseAnimationConfig()
val turbulenceNoiseView = TurbulenceNoiseView(context, null).also { it.applyConfig(config) }
var onAnimationEndCalled = false
fakeExecutor.execute {
turbulenceNoiseView.playEaseOut(onAnimationEnd = { onAnimationEndCalled = true })
fakeSystemClock.advanceTime(config.easeOutDuration.toLong())
assertThat(onAnimationEndCalled).isTrue()
}
}
@Test
fun finish_animationPlaying_finishesAnimation() {
val config = TurbulenceNoiseAnimationConfig()
val turbulenceNoiseView = TurbulenceNoiseView(context, null).also { it.applyConfig(config) }
var onAnimationEndCalled = false
fakeExecutor.execute {
turbulenceNoiseView.play(onAnimationEnd = { onAnimationEndCalled = true })
assertThat(turbulenceNoiseView.currentAnimator).isNotNull()
turbulenceNoiseView.finish()
assertThat(onAnimationEndCalled).isTrue()
assertThat(turbulenceNoiseView.currentAnimator).isNull()
}
}
}