From 9aada02efc8768254054cedf9d4baf3f931b89bc Mon Sep 17 00:00:00 2001 From: David Vacca Date: Mon, 22 Sep 2025 13:24:31 -0700 Subject: [PATCH 1/2] Delete LayoutAnimation tests (#53783) Summary: LayoutAnimation is part of legacy architecture, I'm deleting unused tests changelog: [internal] internal Reviewed By: shwanton Differential Revision: D82254268 --- .../AbstractLayoutAnimationTest.kt | 157 ------------------ .../layoutanimation/InterpolatorTypeTest.kt | 47 ------ .../layoutanimation/OpacityAnimationTest.kt | 105 ------------ 3 files changed, 309 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt delete mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/InterpolatorTypeTest.kt delete mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/OpacityAnimationTest.kt 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 deleted file mode 100644 index cd10239c471b82..00000000000000 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimationTest.kt +++ /dev/null @@ -1,157 +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. - */ - -@file:Suppress("DEPRECATION") - -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.assertj.core.api.Assertions.assertThatThrownBy -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 = 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() - } - - assertThatThrownBy { invalidAnimation.initializeFromConfig(config, 300) } - .isInstanceOf(IllegalViewOperationException::class.java) - .hasMessageContaining("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) - } -} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/InterpolatorTypeTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/InterpolatorTypeTest.kt deleted file mode 100644 index 494cf94b8bdd41..00000000000000 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/InterpolatorTypeTest.kt +++ /dev/null @@ -1,47 +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. - */ - -@file:Suppress("DEPRECATION") - -package com.facebook.react.uimanager.layoutanimation - -import java.util.Locale -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class InterpolatorTypeTest { - @Test - fun testCamelCase() { - assertThat(InterpolatorType.fromString("linear")).isEqualTo(InterpolatorType.LINEAR) - assertThat(InterpolatorType.fromString("easeIn")).isEqualTo(InterpolatorType.EASE_IN) - assertThat(InterpolatorType.fromString("easeOut")).isEqualTo(InterpolatorType.EASE_OUT) - assertThat(InterpolatorType.fromString("easeInEaseOut")) - .isEqualTo(InterpolatorType.EASE_IN_EASE_OUT) - assertThat(InterpolatorType.fromString("spring")).isEqualTo(InterpolatorType.SPRING) - } - - @Test - fun testOtherCases() { - assertThat(InterpolatorType.fromString("EASEIN")).isEqualTo(InterpolatorType.EASE_IN) - assertThat(InterpolatorType.fromString("easeout")).isEqualTo(InterpolatorType.EASE_OUT) - assertThat(InterpolatorType.fromString("easeineaseout")) - .isEqualTo(InterpolatorType.EASE_IN_EASE_OUT) - } - - @Test - fun testLocales() { - Locale.setDefault(Locale.forLanguageTag("tr-TR")) - assertThat(InterpolatorType.fromString("easeInEaseOut")) - .isEqualTo(InterpolatorType.EASE_IN_EASE_OUT) - } - - @Test(expected = IllegalArgumentException::class) - @Throws(IllegalArgumentException::class) - fun testInvalidInterpolatorTypes() { - InterpolatorType.fromString("ease_in_ease_out") - } -} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/OpacityAnimationTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/OpacityAnimationTest.kt deleted file mode 100644 index 45ef6dd5e0332b..00000000000000 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/layoutanimation/OpacityAnimationTest.kt +++ /dev/null @@ -1,105 +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. - */ - -@file:Suppress("DEPRECATION") - -package com.facebook.react.uimanager.layoutanimation - -import android.view.View -import android.view.animation.Transformation -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment - -@RunWith(RobolectricTestRunner::class) -class OpacityAnimationTest { - private lateinit var view: View - private lateinit var animation: OpacityAnimation - - @Before - fun setUp() { - view = View(RuntimeEnvironment.getApplication()) - } - - @Test - fun applyTransformation_updatesAlphaCorrectly() { - val startOpacity = 0.5f - val endOpacity = 1.0f - animation = OpacityAnimation(view, startOpacity, endOpacity) - - val transformation = Transformation() - animation.applyTransformation(0.0f, transformation) - assertThat(view.alpha).isEqualTo(0.5f) - - animation.applyTransformation(0.5f, transformation) - assertThat(view.alpha).isEqualTo(0.75f) - - animation.applyTransformation(1.0f, transformation) - assertThat(view.alpha).isEqualTo(1.0f) - } - - @Test - fun willChangeBounds_returnsFalse() { - animation = OpacityAnimation(view, 0.5f, 1.0f) - assertThat(animation.willChangeBounds()).isFalse() - } - - @Test - fun onAnimationStart_setsLayerTypeHardwareIfOverlappingRendering() { - val spyView: View = mock { - on { hasOverlappingRendering() } doReturn true - on { layerType } doReturn View.LAYER_TYPE_NONE - } - - val listener = OpacityAnimation.OpacityAnimationListener(spyView) - listener.onAnimationStart(mock()) - - verify(spyView).setLayerType(View.LAYER_TYPE_HARDWARE, null) - } - - @Test - fun onAnimationStart_doesNotChangeLayerTypeIfAlreadySet() { - val spyView: View = mock { - on { hasOverlappingRendering() } doReturn true - on { layerType } doReturn View.LAYER_TYPE_HARDWARE - } - - val listener = OpacityAnimation.OpacityAnimationListener(spyView) - listener.onAnimationStart(mock()) - - verify(spyView, never()).setLayerType(View.LAYER_TYPE_HARDWARE, null) - } - - @Test - fun onAnimationEnd_resetsLayerTypeIfChanged() { - val spyView: View = mock { - on { hasOverlappingRendering() } doReturn true - on { layerType } doReturn View.LAYER_TYPE_NONE - } - - val listener = OpacityAnimation.OpacityAnimationListener(spyView) - listener.onAnimationStart(mock()) - listener.onAnimationEnd(mock()) - - verify(spyView).setLayerType(View.LAYER_TYPE_NONE, null) - } - - @Test - fun onAnimationRepeat_doesNothing() { - val listener = OpacityAnimation.OpacityAnimationListener(mock()) - listener.onAnimationRepeat(mock()) - - // No assertions needed, just verifying it runs without errors - } -} From 80fe9dbfa91e36d6fba9ce152ac126dbb652946b Mon Sep 17 00:00:00 2001 From: David Vacca Date: Mon, 22 Sep 2025 13:24:31 -0700 Subject: [PATCH 2/2] Remove code from public LayoutAnimationController class (#53784) Summary: LayoutAnimation is part of legacy architecture, I'm deleting the code used from its non supported public APIs changelog: [internal] internal Reviewed By: shwanton Differential Revision: D82235545 --- .../LayoutAnimationController.kt | 196 ++---------------- 1 file changed, 22 insertions(+), 174 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.kt index eff00259d5de0f..6e7a8088ca2b76 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.kt @@ -9,19 +9,12 @@ package com.facebook.react.uimanager.layoutanimation -import android.util.SparseArray import android.view.View -import android.view.ViewGroup -import android.view.animation.Animation -import android.view.animation.Animation.AnimationListener import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread -import com.facebook.react.bridge.UiThreadUtil.getUiThreadHandler import com.facebook.react.common.annotations.internal.LegacyArchitecture import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel -import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger.assertLegacyArchitecture -import com.facebook.react.uimanager.layoutanimation.LayoutAnimationType.Companion.toString +import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger import javax.annotation.concurrent.NotThreadSafe /** @@ -36,75 +29,27 @@ import javax.annotation.concurrent.NotThreadSafe level = DeprecationLevel.WARNING, ) public open class LayoutAnimationController { - private val layoutCreateAnimation: AbstractLayoutAnimation = LayoutCreateAnimation() - private val layoutUpdateAnimation: AbstractLayoutAnimation = LayoutUpdateAnimation() - private val layoutDeleteAnimation: AbstractLayoutAnimation = LayoutDeleteAnimation() - private val layoutHandlers = SparseArray(0) - - private var shouldAnimateLayout = false - private var maxAnimationDuration: Long = -1 - private var completionRunnable: Runnable? = null public fun initializeFromConfig(config: ReadableMap?, completionCallback: Callback?) { - if (config == null) { - reset() - return - } - - shouldAnimateLayout = false - val globalDuration = if (config.hasKey("duration")) config.getInt("duration") else 0 - if (config.hasKey(toString(LayoutAnimationType.CREATE))) { - layoutCreateAnimation.initializeFromConfig( - config.getMap(toString(LayoutAnimationType.CREATE))!!, - globalDuration, - ) - shouldAnimateLayout = true - } - if (config.hasKey(toString(LayoutAnimationType.UPDATE))) { - layoutUpdateAnimation.initializeFromConfig( - config.getMap(toString(LayoutAnimationType.UPDATE))!!, - globalDuration, - ) - shouldAnimateLayout = true - } - if (config.hasKey(toString(LayoutAnimationType.DELETE))) { - layoutDeleteAnimation.initializeFromConfig( - config.getMap(toString(LayoutAnimationType.DELETE))!!, - globalDuration, - ) - shouldAnimateLayout = true - } - - if (shouldAnimateLayout && completionCallback != null) { - completionRunnable = Runnable { completionCallback.invoke(java.lang.Boolean.TRUE) } - } + LegacyArchitectureLogger.assertLegacyArchitecture( + "LayoutAnimationController", + LegacyArchitectureLogLevel.ERROR, + ) } public open fun reset() { - layoutCreateAnimation.reset() - layoutUpdateAnimation.reset() - layoutDeleteAnimation.reset() - completionRunnable = null - shouldAnimateLayout = false - maxAnimationDuration = -1 - for (i in layoutHandlers.size() - 1 downTo 0) { - val animation = layoutHandlers.valueAt(i) - if (!animation!!.isValid()) { - layoutHandlers.removeAt(i) - } - } + LegacyArchitectureLogger.assertLegacyArchitecture( + "LayoutAnimationController", + LegacyArchitectureLogLevel.ERROR, + ) } public open fun shouldAnimateLayout(viewToAnimate: View?): Boolean { - // if view parent is null, skip animation: view have been clipped, we don't want animation to - // resume when view is re-attached to parent, which is the standard android animation behavior. - // If there's a layout handling animation going on, it should be animated nonetheless since the - // ongoing animation needs to be updated. - if (viewToAnimate == null) { - return false - } - return ((shouldAnimateLayout && viewToAnimate.parent != null) || - layoutHandlers[viewToAnimate.id] != null) + LegacyArchitectureLogger.assertLegacyArchitecture( + "LayoutAnimationController", + LegacyArchitectureLogLevel.ERROR, + ) + return false } /** @@ -119,57 +64,10 @@ public open class LayoutAnimationController { * @param height the new height value for the view */ public open fun applyLayoutUpdate(view: View, x: Int, y: Int, width: Int, height: Int) { - assertOnUiThread() - - val reactTag = view.id - - // Update an ongoing animation if possible, otherwise the layout update would be ignored as - // the existing animation would still animate to the old layout. - val existingAnimation = layoutHandlers[reactTag] - if (existingAnimation != null) { - if (!existingAnimation.isValid()) { - layoutHandlers.remove(reactTag) - } else { - existingAnimation.onLayoutUpdate(x, y, width, height) - return - } - } - - // Determine which animation to use : if view is initially invisible, use create animation, - // otherwise use update animation. This approach is easier than maintaining a list of tags - // for recently created views. - val layoutAnimation = - if ((view.width == 0 || view.height == 0)) layoutCreateAnimation else layoutUpdateAnimation - - val animation = layoutAnimation.createAnimation(view, x, y, width, height) - - if (animation is LayoutHandlingAnimation) { - animation.setAnimationListener( - object : AnimationListener { - override fun onAnimationStart(animation: Animation) { - layoutHandlers.put(reactTag, animation as LayoutHandlingAnimation) - } - - override fun onAnimationEnd(animation: Animation) { - layoutHandlers.remove(reactTag) - } - - override fun onAnimationRepeat(animation: Animation) = Unit - } - ) - } else { - view.layout(x, y, x + width, y + height) - } - - if (animation != null) { - val animationDuration = animation.duration - if (animationDuration > maxAnimationDuration) { - maxAnimationDuration = animationDuration - scheduleCompletionCallback(animationDuration) - } - - view.startAnimation(animation) - } + LegacyArchitectureLogger.assertLegacyArchitecture( + "LayoutAnimationController", + LegacyArchitectureLogLevel.ERROR, + ) } /** @@ -181,59 +79,9 @@ public open class LayoutAnimationController { * view. */ public open fun deleteView(view: View, listener: LayoutAnimationListener) { - assertOnUiThread() - - val animation = - layoutDeleteAnimation.createAnimation(view, view.left, view.top, view.width, view.height) - - if (animation != null) { - disableUserInteractions(view) - - animation.setAnimationListener( - object : AnimationListener { - override fun onAnimationStart(anim: Animation) = Unit - - override fun onAnimationRepeat(anim: Animation) = Unit - - override fun onAnimationEnd(anim: Animation) { - listener.onAnimationEnd() - } - } - ) - - val animationDuration = animation.duration - if (animationDuration > maxAnimationDuration) { - scheduleCompletionCallback(animationDuration) - maxAnimationDuration = animationDuration - } - - view.startAnimation(animation) - } else { - listener.onAnimationEnd() - } - } - - /** Disables user interactions for a view and all it's subviews. */ - private fun disableUserInteractions(view: View) { - view.isClickable = false - if (view is ViewGroup) { - for (i in 0 until view.childCount) { - disableUserInteractions(view.getChildAt(i)) - } - } - } - - private fun scheduleCompletionCallback(delayMillis: Long) { - if (completionRunnable != null) { - val completionHandler = getUiThreadHandler() - completionHandler.removeCallbacks(completionRunnable!!) - completionHandler.postDelayed(completionRunnable!!, delayMillis) - } - } - - private companion object { - init { - assertLegacyArchitecture("LayoutAnimationController", LegacyArchitectureLogLevel.ERROR) - } + LegacyArchitectureLogger.assertLegacyArchitecture( + "LayoutAnimationController", + LegacyArchitectureLogLevel.ERROR, + ) } }