Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.uimanager.layoutanimation

import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AccelerateInterpolator
import android.view.animation.Animation
import android.view.animation.BaseInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.view.animation.LinearInterpolator
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.annotations.VisibleForTesting
import com.facebook.react.common.annotations.internal.LegacyArchitecture
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
import com.facebook.react.uimanager.IllegalViewOperationException

/**
* Class responsible for parsing and converting layout animation data into native [Animation]
* in order to animate layout when a valid configuration has been supplied by the application.
*/
@LegacyArchitecture
internal abstract class AbstractLayoutAnimation {
var interpolator: Interpolator? = null
var delayMs: Int = 0
var animatedProperty: AnimatedPropertyType? = null
var durationMs: Int = 0

internal abstract fun isValid(): Boolean

/**
* Create an animation object for the current animation type, based on the view and final screen
* coordinates. If the application-supplied configuration does not specify an animation definition
* for this types, or if the animation definition is invalid, returns null.
*/
internal abstract fun createAnimationImpl(
view: View,
x: Int,
y: Int,
width: Int,
height: Int
): Animation?

fun reset() {
animatedProperty = null
durationMs = 0
delayMs = 0
interpolator = null
}

fun initializeFromConfig(data: ReadableMap, globalDuration: Int) {
animatedProperty = if (data.hasKey("property")) {
AnimatedPropertyType.fromString(data.getString("property") ?: "")
} else {
null
}
durationMs = if (data.hasKey("duration")) data.getInt("duration") else globalDuration
delayMs = if (data.hasKey("delay")) data.getInt("delay") else 0
require(data.hasKey("type")) { "Missing interpolation type." }

interpolator = getInterpolator(InterpolatorType.fromString(data.getString("type") ?: ""), data)

if (!isValid()) {
throw IllegalViewOperationException("Invalid layout animation : $data")
}
}

/**
* Create an animation object to be used to animate the view, based on the animation config
* supplied at initialization time and the new view position and size.
*
* @param view the view to create the animation for
* @param x the new X position for the view
* @param y the new Y position for the view
* @param width the new width value for the view
* @param height the new height value for the view
*/
fun createAnimation(view: View, x: Int, y: Int, width: Int, height: Int): Animation? {
if (!isValid()) {
return null
}

return createAnimationImpl(view, x, y, width, height)?.apply {
val slowdownFactor = if (SLOWDOWN_ANIMATION_MODE) 10 else 1
duration = (durationMs * slowdownFactor).toLong()
startOffset = (delayMs * slowdownFactor).toLong()
interpolator = interpolator
}
}

companion object {
init {
LegacyArchitectureLogger.assertWhenLegacyArchitectureMinifyingEnabled(
"AbstractLayoutAnimation", LegacyArchitectureLogLevel.WARNING
)
}

// Forces animation to be playing 10x slower, used for debug purposes.
private const val SLOWDOWN_ANIMATION_MODE = false

private val INTERPOLATOR: Map<InterpolatorType, BaseInterpolator> = mapOf(
InterpolatorType.LINEAR to LinearInterpolator(),
InterpolatorType.EASE_IN to AccelerateInterpolator(),
InterpolatorType.EASE_OUT to DecelerateInterpolator(),
InterpolatorType.EASE_IN_EASE_OUT to AccelerateDecelerateInterpolator()
)

@VisibleForTesting
fun getInterpolator(type: InterpolatorType, params: ReadableMap): Interpolator {
val interpolator = if (type == InterpolatorType.SPRING) {
SimpleSpringInterpolator(SimpleSpringInterpolator.getSpringDamping(params))
} else {
INTERPOLATOR[type]
}
requireNotNull(interpolator) { "Missing interpolator for type : $type" }
return interpolator
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import com.facebook.react.uimanager.IllegalViewOperationException
internal abstract class BaseLayoutAnimation : AbstractLayoutAnimation() {
abstract fun isReverse(): Boolean

override fun isValid(): Boolean = mDurationMs > 0 && mAnimatedProperty != null
override fun isValid(): Boolean = durationMs > 0 && animatedProperty != null

override fun createAnimationImpl(view: View, x: Int, y: Int, width: Int, height: Int): Animation {
mAnimatedProperty?.let {
animatedProperty?.let {
return when (it) {
AnimatedPropertyType.OPACITY -> {
val fromValue = if (isReverse()) view.alpha else 0.0f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger
@LegacyArchitecture
internal class LayoutUpdateAnimation : AbstractLayoutAnimation() {

internal override fun isValid(): Boolean = mDurationMs > 0
internal override fun isValid(): Boolean = durationMs > 0

internal override fun createAnimationImpl(
view: View,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.uimanager.layoutanimation

import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.uimanager.IllegalViewOperationException
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.junit.Assert.assertThrows

class AbstractLayoutAnimationTest {
private lateinit var view: View
private lateinit var config: ReadableMap
private lateinit var animation: AbstractLayoutAnimation

@Before
fun setUp() {
view = mock()
config = mock()

animation = object : AbstractLayoutAnimation() {
override fun isValid(): Boolean = true

override fun createAnimationImpl(
view: View, x: Int, y: Int, width: Int, height: Int
): Animation? {
return mock()
}
}
}

@Test
fun reset_clearsAnimationProperties() {
animation.reset()
assertThat(animation.animatedProperty).isNull()
assertThat(animation.durationMs).isEqualTo(0)
assertThat(animation.delayMs).isEqualTo(0)
assertThat(animation.interpolator).isNull()
}

@Test
fun createAnimation_returnsValidAnimation() {
val result = animation.createAnimation(view, 0, 0, 100, 100)
assertThat(result).isNotNull
}

@Test
fun initializeFromConfig_throwsIfTypeMissing() {
whenever(config.hasKey("type")).thenReturn(false)

val exception = assertThrows(IllegalArgumentException::class.java) {
animation.initializeFromConfig(config, 300)
}
assertThat(exception.message).isEqualTo("Missing interpolation type.")
}

@Test
fun createAnimation_returnsNullWhenInvalid() {
val invalidAnimation = object : AbstractLayoutAnimation() {
override fun isValid(): Boolean = false
override fun createAnimationImpl(
view: View, x: Int, y: Int, width: Int, height: Int
): Animation? = mock()
}

val result = invalidAnimation.createAnimation(view, 0, 0, 100, 100)
assertThat(result).isNull()
}

@Test
fun initializeFromConfig_throwsIfInvalidAnimation() {
whenever(config.hasKey("type")).thenReturn(true)
whenever(config.getString("type")).thenReturn("linear")
whenever(config.hasKey("duration")).thenReturn(true)
whenever(config.getInt("duration")).thenReturn(300)

val invalidAnimation = object : AbstractLayoutAnimation() {
override fun isValid(): Boolean = false
override fun createAnimationImpl(
view: View, x: Int, y: Int, width: Int, height: Int
): Animation? = mock()
}

val exception = assertThrows(IllegalViewOperationException::class.java) {
invalidAnimation.initializeFromConfig(config, 300)
}
assertThat(exception.message).contains("Invalid layout animation")
}

@Test
fun getInterpolator_returnsSimpleSpringInterpolator() {
val type = InterpolatorType.SPRING
val params = mock<ReadableMap>()
whenever(params.hasKey("damping")).thenReturn(true)
whenever(params.getDouble("damping")).thenReturn(0.5)

val interpolator = AbstractLayoutAnimation.getInterpolator(type, params)
assertThat(interpolator).isInstanceOf(SimpleSpringInterpolator::class.java)
}

@Test
fun getInterpolator_returnsDefaultInterpolator() {
val type = InterpolatorType.LINEAR
val params = mock<ReadableMap>()

val interpolator = AbstractLayoutAnimation.getInterpolator(type, params)
assertThat(interpolator).isInstanceOf(LinearInterpolator::class.java)
}
}