Migrate OpacityAnimation to Kotlin (#49822)

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

I moved out `OpacityAnimationListener` as well in this PR to separate concerns and align more with the codebase.

## Changelog:

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

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

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

Reviewed By: javache

Differential Revision: D70588884

Pulled By: alanleedev

fbshipit-source-id: 1b70f5e5facd5f47b828090e7c63a218229000cb
This commit is contained in:
Mateo Guzmán
2025-03-11 08:40:10 -07:00
committed by Facebook GitHub Bot
parent bf7a0e5798
commit ff84ae9188
3 changed files with 168 additions and 81 deletions
@@ -1,81 +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.Animation;
import android.view.animation.Transformation;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.common.annotations.internal.LegacyArchitecture;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogLevel;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
/**
* Animation responsible for updating opacity of a view. It should ideally use hardware texture to
* optimize rendering performances.
*/
/* package */ @Nullsafe(Nullsafe.Mode.LOCAL)
@LegacyArchitecture
class OpacityAnimation extends Animation {
static {
LegacyArchitectureLogger.assertWhenLegacyArchitectureMinifyingEnabled(
"OpacityAnimation", LegacyArchitectureLogLevel.WARNING);
}
static class OpacityAnimationListener implements Animation.AnimationListener {
private final View mView;
private boolean mLayerTypeChanged = false;
public OpacityAnimationListener(View view) {
mView = view;
}
@Override
public void onAnimationStart(Animation animation) {
if (mView.hasOverlappingRendering() && mView.getLayerType() == View.LAYER_TYPE_NONE) {
mLayerTypeChanged = true;
mView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
@Override
public void onAnimationEnd(Animation animation) {
if (mLayerTypeChanged) {
mView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
// do nothing
}
}
private final View mView;
private final float mStartOpacity, mDeltaOpacity;
public OpacityAnimation(View view, float startOpacity, float endOpacity) {
mView = view;
mStartOpacity = startOpacity;
mDeltaOpacity = endOpacity - startOpacity;
setAnimationListener(new OpacityAnimationListener(view));
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mView.setAlpha(mStartOpacity + mDeltaOpacity * interpolatedTime);
}
@Override
public boolean willChangeBounds() {
return false;
}
}
@@ -0,0 +1,65 @@
/*
* 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.Transformation
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
/**
* Animation responsible for updating opacity of a view. It should ideally use hardware texture to
* optimize rendering performances.
*/
@LegacyArchitecture
internal class OpacityAnimation(
private val view: View,
private val startOpacity: Float,
endOpacity: Float
) : Animation() {
private val deltaOpacity = endOpacity - startOpacity
init {
setAnimationListener(OpacityAnimationListener(view))
LegacyArchitectureLogger.assertWhenLegacyArchitectureMinifyingEnabled(
"OpacityAnimation", LegacyArchitectureLogLevel.WARNING)
}
class OpacityAnimationListener(private val view: View) : Animation.AnimationListener {
private var layerTypeChanged = false
override fun onAnimationStart(animation: Animation) {
if (view.hasOverlappingRendering() && view.layerType == View.LAYER_TYPE_NONE) {
layerTypeChanged = true
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
}
}
override fun onAnimationEnd(animation: Animation) {
if (layerTypeChanged) {
view.setLayerType(View.LAYER_TYPE_NONE, null)
}
}
override fun onAnimationRepeat(animation: Animation) {
// do nothing
}
}
@VisibleForTesting
public override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
view.alpha = startOpacity + deltaOpacity * interpolatedTime
}
override fun willChangeBounds(): Boolean {
return false
}
}
@@ -0,0 +1,103 @@
/*
* 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.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
}
}