diff --git a/RNTester/js/LayoutAnimationExample.js b/RNTester/js/LayoutAnimationExample.js
index 92134409879afe..d815712bba4f0b 100644
--- a/RNTester/js/LayoutAnimationExample.js
+++ b/RNTester/js/LayoutAnimationExample.js
@@ -102,6 +102,57 @@ class CrossFadeExample extends React.Component<{}, $FlowFixMeState> {
}
}
+class LayoutUpdateExample extends React.Component<{}, $FlowFixMeState> {
+ state = {
+ width: 200,
+ height: 100,
+ };
+
+ timeout = null;
+
+ componentWillUnmount() {
+ this._clearTimeout();
+ }
+
+ _clearTimeout = () => {
+ if (this.timeout !== null) {
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ }
+ }
+
+ _onPressToggle = () => {
+ this._clearTimeout();
+ this.setState({width: 150});
+
+ LayoutAnimation.configureNext({
+ duration: 1000,
+ update: {
+ type: LayoutAnimation.Types.linear,
+ },
+ });
+
+ this.timeout = setTimeout(() => this.setState({width: 100}), 500);
+ };
+
+ render() {
+ const {width, height} = this.state;
+
+ return (
+
+
+
+ Make box square
+
+
+
+ {width}x{height}
+
+
+ );
+ }
+}
+
const styles = StyleSheet.create({
container: {
flex: 1,
@@ -156,4 +207,9 @@ exports.examples = [{
render(): React.Element {
return ;
},
+}, {
+ title: 'Layout update during animation',
+ render(): React.Element {
+ return ;
+ },
}];
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/HandlesLayout.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/HandlesLayout.java
deleted file mode 100644
index 9f3c907e97d079..00000000000000
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/HandlesLayout.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright 2004-present Facebook. All Rights Reserved.
-
-package com.facebook.react.uimanager.layoutanimation;
-
-/**
- * Marker interface to indicate a given animation type takes care of updating the view layout.
- */
-/* package */ interface HandleLayout {
-
-}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java
index d5a08e6b264ac5..7c88514f90031e 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java
@@ -5,6 +5,7 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
+import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@@ -27,6 +28,7 @@ public class LayoutAnimationController {
private final AbstractLayoutAnimation mLayoutCreateAnimation = new LayoutCreateAnimation();
private final AbstractLayoutAnimation mLayoutUpdateAnimation = new LayoutUpdateAnimation();
private final AbstractLayoutAnimation mLayoutDeleteAnimation = new LayoutDeleteAnimation();
+ private final SparseArray mLayoutHandlers = new SparseArray<>(0);
private boolean mShouldAnimateLayout;
public void initializeFromConfig(final @Nullable ReadableMap config) {
@@ -68,7 +70,10 @@ public void reset() {
public boolean shouldAnimateLayout(View viewToAnimate) {
// 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.
- return mShouldAnimateLayout && viewToAnimate.getParent() != null;
+ // If there's a layout handling animation going on, it should be animated nonetheless since the
+ // ongoing animation needs to be updated.
+ return (mShouldAnimateLayout && viewToAnimate.getParent() != null)
+ || mLayoutHandlers.get(viewToAnimate.getId()) != null;
}
/**
@@ -85,6 +90,16 @@ public boolean shouldAnimateLayout(View viewToAnimate) {
public void applyLayoutUpdate(View view, int x, int y, int width, int height) {
UiThreadUtil.assertOnUiThread();
+ final int reactTag = view.getId();
+ LayoutHandlingAnimation existingAnimation = mLayoutHandlers.get(reactTag);
+
+ // Update an ongoing animation if possible, otherwise the layout update would be ignored as
+ // the existing animation would still animate to the old layout.
+ if (existingAnimation != null) {
+ 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.
@@ -93,9 +108,26 @@ public void applyLayoutUpdate(View view, int x, int y, int width, int height) {
mLayoutUpdateAnimation;
Animation animation = layoutAnimation.createAnimation(view, x, y, width, height);
- if (animation == null || !(animation instanceof HandleLayout)) {
+
+ if (animation instanceof LayoutHandlingAnimation) {
+ animation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mLayoutHandlers.put(reactTag, (LayoutHandlingAnimation) animation);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mLayoutHandlers.remove(reactTag);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ } else {
view.layout(x, y, x + width, y + height);
}
+
if (animation != null) {
view.startAnimation(animation);
}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutHandlingAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutHandlingAnimation.java
new file mode 100644
index 00000000000000..69234e27029ac7
--- /dev/null
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutHandlingAnimation.java
@@ -0,0 +1,20 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.uimanager.layoutanimation;
+
+/**
+ * Interface for an animation type that takes care of updating the view layout.
+ */
+/* package */ interface LayoutHandlingAnimation {
+ /**
+ * Notifies the animation of a layout update in case one occurs during the animation. This
+ * avoids animating the view to the old layout since it's no longer correct; instead the
+ * animation should update and do whatever it can so that the final layout is correct.
+ *
+ * @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
+ */
+ void onLayoutUpdate(int x, int y, int width, int height);
+}
diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/PositionAndSizeAnimation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/PositionAndSizeAnimation.java
index 1cb3e83ac6f858..9bce9f781627ed 100644
--- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/PositionAndSizeAnimation.java
+++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/PositionAndSizeAnimation.java
@@ -12,24 +12,15 @@
* layout passes occurring on every frame.
* What we might want to try to do instead is use a combined ScaleAnimation and TranslateAnimation.
*/
-/* package */ class PositionAndSizeAnimation extends Animation implements HandleLayout {
+/* package */ class PositionAndSizeAnimation extends Animation implements LayoutHandlingAnimation {
private final View mView;
- private final float mStartX, mStartY, mDeltaX, mDeltaY;
- private final int mStartWidth, mStartHeight, mDeltaWidth, mDeltaHeight;
+ private float mStartX, mStartY, mDeltaX, mDeltaY;
+ private int mStartWidth, mStartHeight, mDeltaWidth, mDeltaHeight;
public PositionAndSizeAnimation(View view, int x, int y, int width, int height) {
mView = view;
-
- mStartX = view.getX() - view.getTranslationX();
- mStartY = view.getY() - view.getTranslationY();
- mStartWidth = view.getWidth();
- mStartHeight = view.getHeight();
-
- mDeltaX = x - mStartX;
- mDeltaY = y - mStartY;
- mDeltaWidth = width - mStartWidth;
- mDeltaHeight = height - mStartHeight;
+ calculateAnimation(x, y, width, height);
}
@Override
@@ -45,8 +36,27 @@ protected void applyTransformation(float interpolatedTime, Transformation t) {
Math.round(newY + newHeight));
}
+ @Override
+ public void onLayoutUpdate(int x, int y, int width, int height) {
+ // Layout changed during the animation, we should update our values so that the final layout
+ // is correct.
+ calculateAnimation(x, y, width, height);
+ }
+
@Override
public boolean willChangeBounds() {
return true;
}
+
+ private void calculateAnimation(int x, int y, int width, int height) {
+ mStartX = mView.getX() - mView.getTranslationX();
+ mStartY = mView.getY() - mView.getTranslationY();
+ mStartWidth = mView.getWidth();
+ mStartHeight = mView.getHeight();
+
+ mDeltaX = x - mStartX;
+ mDeltaY = y - mStartY;
+ mDeltaWidth = width - mStartWidth;
+ mDeltaHeight = height - mStartHeight;
+ }
}