Migrate AbstractLayoutAnimation to Kotlin (#50145)

Summary:
Migrate com.facebook.react.uimanager.layoutanimation.AbstractLayoutAnimation to Kotlin.

## Changelog:

[INTERNAL] - Migrate com.facebook.react.uimanager.layoutanimation.AbstractLayoutAnimation to Kotlin

Pull Request resolved: https://github.com/facebook/react-native/pull/50145

Test Plan:
```bash
yarn test-android
yarn android
```

Reviewed By: alanleedev

Differential Revision: D71541347

Pulled By: javache

fbshipit-source-id: 5b53f9076a732afc68911f2ace7209f0fee46292
This commit is contained in:
Mateo Guzmán
2025-03-20 17:00:55 -07:00
committed by Facebook GitHub Bot
parent 45fa563d3a
commit ad0205a122
5 changed files with 289 additions and 128 deletions
@@ -1,125 +0,0 @@
/*
* 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 androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
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;
import java.util.Map;
/**
* Class responsible for parsing and converting layout animation data into native {@link Animation}
* in order to animate layout when a valid configuration has been supplied by the application.
*/
@LegacyArchitecture
/* package */ abstract class AbstractLayoutAnimation {
static {
LegacyArchitectureLogger.assertWhenLegacyArchitectureMinifyingEnabled(
"AbstractLayoutAnimation", LegacyArchitectureLogLevel.WARNING);
}
// Forces animation to be playing 10x slower, used for debug purposes.
private static final boolean SLOWDOWN_ANIMATION_MODE = false;
abstract boolean isValid();
/**
* 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.
*/
abstract @Nullable Animation createAnimationImpl(View view, int x, int y, int width, int height);
private static final Map<InterpolatorType, BaseInterpolator> INTERPOLATOR =
MapBuilder.of(
InterpolatorType.LINEAR, new LinearInterpolator(),
InterpolatorType.EASE_IN, new AccelerateInterpolator(),
InterpolatorType.EASE_OUT, new DecelerateInterpolator(),
InterpolatorType.EASE_IN_EASE_OUT, new AccelerateDecelerateInterpolator());
private @Nullable Interpolator mInterpolator;
private int mDelayMs;
protected @Nullable AnimatedPropertyType mAnimatedProperty;
protected int mDurationMs;
public void reset() {
mAnimatedProperty = null;
mDurationMs = 0;
mDelayMs = 0;
mInterpolator = null;
}
public void initializeFromConfig(ReadableMap data, int globalDuration) {
mAnimatedProperty =
data.hasKey("property")
? AnimatedPropertyType.fromString(data.getString("property"))
: null;
mDurationMs = data.hasKey("duration") ? data.getInt("duration") : globalDuration;
mDelayMs = data.hasKey("delay") ? data.getInt("delay") : 0;
if (!data.hasKey("type")) {
throw new IllegalArgumentException("Missing interpolation type.");
}
mInterpolator = getInterpolator(InterpolatorType.fromString(data.getString("type")), data);
if (!isValid()) {
throw new 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
*/
public final @Nullable Animation createAnimation(View view, int x, int y, int width, int height) {
if (!isValid()) {
return null;
}
Animation animation = createAnimationImpl(view, x, y, width, height);
if (animation != null) {
int slowdownFactor = SLOWDOWN_ANIMATION_MODE ? 10 : 1;
animation.setDuration(mDurationMs * slowdownFactor);
animation.setStartOffset(mDelayMs * slowdownFactor);
animation.setInterpolator(mInterpolator);
}
return animation;
}
private static Interpolator getInterpolator(InterpolatorType type, ReadableMap params) {
Interpolator interpolator;
if (type.equals(InterpolatorType.SPRING)) {
interpolator =
new SimpleSpringInterpolator(SimpleSpringInterpolator.getSpringDamping(params));
} else {
interpolator = INTERPOLATOR.get(type);
}
if (interpolator == null) {
throw new IllegalArgumentException("Missing interpolator for type : " + type);
}
return interpolator;
}
}
@@ -0,0 +1,128 @@
/*
* 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").orEmpty())
} 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").orEmpty()), 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 = this@AbstractLayoutAnimation.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
}
}
}
@@ -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
@@ -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,
@@ -0,0 +1,158 @@
/*
* 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.DecelerateInterpolator
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.After
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mockConstruction
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
class AbstractLayoutAnimationTest {
private lateinit var view: View
private lateinit var config: ReadableMap
private lateinit var animation: AbstractLayoutAnimation
private lateinit var mockedConstructors: List<AutoCloseable>
@Before
fun setUp() {
view = mock()
config = mock()
mockedConstructors =
listOf(
mockConstruction(LinearInterpolator::class.java),
mockConstruction(AccelerateInterpolator::class.java),
mockConstruction(DecelerateInterpolator::class.java),
mockConstruction(AccelerateDecelerateInterpolator::class.java),
)
animation =
object : AbstractLayoutAnimation() {
override fun isValid(): Boolean = true
override fun createAnimationImpl(
view: View,
x: Int,
y: Int,
width: Int,
height: Int
): Animation? {
return mock()
}
}
}
@After
fun tearDown() {
mockedConstructors.forEach { it.close() }
}
@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)
}
}