Skip to content

Commit

Permalink
Implement completion callback for LayoutAnimation on Android
Browse files Browse the repository at this point in the history
Summary: All animations are scheduled by the UIManager while it processes a batch of changes, so we can just wait to see what the longest animation is and cancel+reschedule the callback.

Reviewed By: mdvacca

Differential Revision: D14656733

fbshipit-source-id: 4cbbb7e741219cd43f511f2ce750c53c30e2b2ca
  • Loading branch information
javache authored and facebook-github-bot committed Apr 3, 2019
1 parent a333c2b commit f571c62
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 37 deletions.
4 changes: 1 addition & 3 deletions Libraries/LayoutAnimation/LayoutAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ function configureNext(
UIManager.configureNextLayoutAnimation(
config,
onAnimationDidEnd ?? function() {},
function() {
/* unused */
},
function() {} /* unused onError */,
);
}
}
Expand Down
21 changes: 14 additions & 7 deletions RNTester/js/LayoutAnimationExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class AddRemoveExample extends React.Component<{}, $FlowFixMeState> {
};

UNSAFE_componentWillUpdate() {
LayoutAnimation.easeInEaseOut();
LayoutAnimation.easeInEaseOut(args =>
console.log('AddRemoveExample completed', args),
);
}

_onPressAddView = () => {
Expand Down Expand Up @@ -73,7 +75,9 @@ class CrossFadeExample extends React.Component<{}, $FlowFixMeState> {
};

_onPressToggle = () => {
LayoutAnimation.easeInEaseOut();
LayoutAnimation.easeInEaseOut(args =>
console.log('CrossFadeExample completed', args),
);
this.setState(state => ({toggled: !state.toggled}));
};

Expand Down Expand Up @@ -116,12 +120,15 @@ class LayoutUpdateExample extends React.Component<{}, $FlowFixMeState> {
this._clearTimeout();
this.setState({width: 150});

LayoutAnimation.configureNext({
duration: 1000,
update: {
type: LayoutAnimation.Types.linear,
LayoutAnimation.configureNext(
{
duration: 1000,
update: {
type: LayoutAnimation.Types.linear,
},
},
});
args => console.log('LayoutUpdateExample completed', args),
);

this.timeout = setTimeout(() => this.setState({width: 100}), 500);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,8 @@ public void clearJSResponder() {
mJSResponderHandler.clearJSResponder();
}

void configureLayoutAnimation(final ReadableMap config) {
mLayoutAnimator.initializeFromConfig(config);
void configureLayoutAnimation(final ReadableMap config, final Callback onAnimationComplete) {
mLayoutAnimator.initializeFromConfig(config, onAnimationComplete);
}

void clearLayoutAnimation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,11 +720,8 @@ public void setLayoutAnimationEnabledExperimental(boolean enabled) {
* interrupted. In this case, callback parameter will be false.
* @param error will be called if there was an error processing the animation
*/
public void configureNextLayoutAnimation(
ReadableMap config,
Callback success,
Callback error) {
mOperationsQueue.enqueueConfigureLayoutAnimation(config, success, error);
public void configureNextLayoutAnimation(ReadableMap config, Callback success) {
mOperationsQueue.enqueueConfigureLayoutAnimation(config, success);
}

public void setJSResponder(int reactTag, boolean blockNativeResponder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,7 @@ public void setLayoutAnimationEnabledExperimental(boolean enabled) {
* Configure an animation to be used for the native layout changes, and native views creation. The
* animation will only apply during the current batch operations.
*
* <p>TODO(7728153) : animating view deletion is currently not supported. TODO(7613721) :
* callbacks are not supported, this feature will likely be killed.
* <p>TODO(7728153) : animating view deletion is currently not supported.
*
* @param config the configuration of the animation for view addition/removal/update.
* @param success will be called when the animation completes, or when the animation get
Expand All @@ -719,7 +718,7 @@ public void setLayoutAnimationEnabledExperimental(boolean enabled) {
*/
@ReactMethod
public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) {
mUIImplementation.configureNextLayoutAnimation(config, success, error);
mUIImplementation.configureNextLayoutAnimation(config, success);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,14 +369,16 @@ public void execute() {

private class ConfigureLayoutAnimationOperation implements UIOperation {
private final ReadableMap mConfig;
private final Callback mAnimationComplete;

private ConfigureLayoutAnimationOperation(final ReadableMap config) {
private ConfigureLayoutAnimationOperation(final ReadableMap config, final Callback animationComplete) {
mConfig = config;
mAnimationComplete = animationComplete;
}

@Override
public void execute() {
mNativeViewHierarchyManager.configureLayoutAnimation(mConfig);
mNativeViewHierarchyManager.configureLayoutAnimation(mConfig, mAnimationComplete);
}
}

Expand Down Expand Up @@ -741,9 +743,8 @@ public void enqueueSetLayoutAnimationEnabled(

public void enqueueConfigureLayoutAnimation(
final ReadableMap config,
final Callback onSuccess,
final Callback onError) {
mOperations.add(new ConfigureLayoutAnimationOperation(config));
final Callback onAnimationComplete) {
mOperations.add(new ConfigureLayoutAnimationOperation(config, onAnimationComplete));
}

public void enqueueMeasure(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,37 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import android.os.Handler;
import android.os.Looper;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;

/**
* Class responsible for animation layout changes, if a valid layout animation config has been
* supplied. If not animation is available, layout change is applied immediately instead of
* performing an animation.
*
* TODO(7613721): Invoke success callback at the end of animation and when animation gets cancelled.
*/
@NotThreadSafe
public class LayoutAnimationController {

private static final boolean ENABLED = true;

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;
private long mMaxAnimationDuration = -1;
@Nullable private Runnable mCompletionRunnable;

public void initializeFromConfig(final @Nullable ReadableMap config) {
if (!ENABLED) {
return;
}
@Nullable private static Handler sCompletionHandler;

public void initializeFromConfig(final @Nullable ReadableMap config, final Callback completionCallback) {
if (config == null) {
reset();
return;
Expand All @@ -61,13 +61,24 @@ public void initializeFromConfig(final @Nullable ReadableMap config) {
config.getMap(LayoutAnimationType.toString(LayoutAnimationType.DELETE)), globalDuration);
mShouldAnimateLayout = true;
}

if (mShouldAnimateLayout && completionCallback != null) {
mCompletionRunnable = new Runnable() {
@Override
public void run() {
completionCallback.invoke(Boolean.TRUE);
}
};
}
}

public void reset() {
mLayoutCreateAnimation.reset();
mLayoutUpdateAnimation.reset();
mLayoutDeleteAnimation.reset();
mCompletionRunnable = null;
mShouldAnimateLayout = false;
mMaxAnimationDuration = -1;
}

public boolean shouldAnimateLayout(View viewToAnimate) {
Expand All @@ -94,10 +105,10 @@ 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.
LayoutHandlingAnimation existingAnimation = mLayoutHandlers.get(reactTag);
if (existingAnimation != null) {
existingAnimation.onLayoutUpdate(x, y, width, height);
return;
Expand Down Expand Up @@ -132,6 +143,12 @@ public void onAnimationRepeat(Animation animation) {}
}

if (animation != null) {
long animationDuration = animation.getDuration();
if (animationDuration > mMaxAnimationDuration) {
mMaxAnimationDuration = animationDuration;
scheduleCompletionCallback(animationDuration);
}

view.startAnimation(animation);
}
}
Expand All @@ -146,9 +163,7 @@ public void onAnimationRepeat(Animation animation) {}
public void deleteView(final View view, final LayoutAnimationListener listener) {
UiThreadUtil.assertOnUiThread();

AbstractLayoutAnimation layoutAnimation = mLayoutDeleteAnimation;

Animation animation = layoutAnimation.createAnimation(
Animation animation = mLayoutDeleteAnimation.createAnimation(
view, view.getLeft(), view.getTop(), view.getWidth(), view.getHeight());

if (animation != null) {
Expand All @@ -167,6 +182,12 @@ public void onAnimationEnd(Animation anim) {
}
});

long animationDuration = animation.getDuration();
if (animationDuration > mMaxAnimationDuration) {
scheduleCompletionCallback(animationDuration);
mMaxAnimationDuration = animationDuration;
}

view.startAnimation(animation);
} else {
listener.onAnimationEnd();
Expand All @@ -185,4 +206,15 @@ private void disableUserInteractions(View view) {
}
}
}

private void scheduleCompletionCallback(long delayMillis) {
if (sCompletionHandler == null) {
sCompletionHandler = new Handler(Looper.getMainLooper());
}

if (mCompletionRunnable != null) {
sCompletionHandler.removeCallbacks(mCompletionRunnable);
sCompletionHandler.postDelayed(mCompletionRunnable, delayMillis);
}
}
}

0 comments on commit f571c62

Please sign in to comment.