Skip to content

Handle layout updates during LayoutAnimation animations on Android #18651

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions RNTester/js/LayoutAnimationExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<View style={styles.container}>
<TouchableOpacity onPress={this._onPressToggle}>
<View style={styles.button}>
<Text>Make box square</Text>
</View>
</TouchableOpacity>
<View style={[styles.view, {width, height}]}>
<Text>{width}x{height}</Text>
</View>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -156,4 +207,9 @@ exports.examples = [{
render(): React.Element<any> {
return <CrossFadeExample />;
},
}, {
title: 'Layout update during animation',
render(): React.Element<any> {
return <LayoutUpdateExample />;
},
}];

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<LayoutHandlingAnimation> mLayoutHandlers = new SparseArray<>(0);
private boolean mShouldAnimateLayout;

public void initializeFromConfig(final @Nullable ReadableMap config) {
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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.
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}