From ad0205a122d337f471ed5cb2923547dbd02c105e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Guzm=C3=A1n?= Date: Thu, 20 Mar 2025 17:00:55 -0700 Subject: [PATCH] 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 --- .../AbstractLayoutAnimation.java | 125 -------------- .../AbstractLayoutAnimation.kt | 128 ++++++++++++++ .../layoutanimation/BaseLayoutAnimation.kt | 4 +- .../layoutanimation/LayoutUpdateAnimation.kt | 2 +- .../AbstractLayoutAnimationTest.kt | 158 ++++++++++++++++++ 5 files changed, 289 insertions(+), 128 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.kt create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java deleted file mode 100644 index d71bb36e974..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java +++ /dev/null @@ -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 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; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.kt new file mode 100644 index 00000000000..dc7083b06e0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.kt @@ -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 = + 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 + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/BaseLayoutAnimation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/BaseLayoutAnimation.kt index ff4a8107f38..2093712d1e7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/BaseLayoutAnimation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/BaseLayoutAnimation.kt @@ -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 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutUpdateAnimation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutUpdateAnimation.kt index 1658a5d939e..f1c6f6dc89e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutUpdateAnimation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutUpdateAnimation.kt @@ -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, diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt new file mode 100644 index 00000000000..5320abd2123 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt @@ -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 + + @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() + 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() + + val interpolator = AbstractLayoutAnimation.getInterpolator(type, params) + assertThat(interpolator).isInstanceOf(LinearInterpolator::class.java) + } +}