mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
45fa563d3a
commit
ad0205a122
-125
@@ -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;
|
||||
}
|
||||
}
|
||||
+128
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+158
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user