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; + } }