Implement completion callback for LayoutAnimation on Android

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
This commit is contained in:
Pieter De Baets
2019-04-03 04:38:50 -07:00
committed by Facebook Github Bot
parent a333c2b202
commit f571c62ddf
7 changed files with 71 additions and 37 deletions

View File

@@ -47,9 +47,7 @@ function configureNext(
UIManager.configureNextLayoutAnimation(
config,
onAnimationDidEnd ?? function() {},
function() {
/* unused */
},
function() {} /* unused onError */,
);
}
}

View File

@@ -20,7 +20,9 @@ class AddRemoveExample extends React.Component<{}, $FlowFixMeState> {
};
UNSAFE_componentWillUpdate() {
LayoutAnimation.easeInEaseOut();
LayoutAnimation.easeInEaseOut(args =>
console.log('AddRemoveExample completed', args),
);
}
_onPressAddView = () => {
@@ -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}));
};
@@ -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);
};

View File

@@ -729,8 +729,8 @@ public class NativeViewHierarchyManager {
mJSResponderHandler.clearJSResponder();
}
void configureLayoutAnimation(final ReadableMap config) {
mLayoutAnimator.initializeFromConfig(config);
void configureLayoutAnimation(final ReadableMap config, final Callback onAnimationComplete) {
mLayoutAnimator.initializeFromConfig(config, onAnimationComplete);
}
void clearLayoutAnimation() {

View File

@@ -720,11 +720,8 @@ public class UIImplementation {
* 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) {

View File

@@ -709,8 +709,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule
* 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
@@ -719,7 +718,7 @@ public class UIManagerModule extends ReactContextBaseJavaModule
*/
@ReactMethod
public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) {
mUIImplementation.configureNextLayoutAnimation(config, success, error);
mUIImplementation.configureNextLayoutAnimation(config, success);
}
/**

View File

@@ -369,14 +369,16 @@ public class UIViewOperationQueue {
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);
}
}
@@ -741,9 +743,8 @@ public class UIViewOperationQueue {
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(

View File

@@ -8,11 +8,14 @@ package com.facebook.react.uimanager.layoutanimation;
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;
@@ -20,25 +23,22 @@ 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;
@@ -61,13 +61,24 @@ public class LayoutAnimationController {
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) {
@@ -94,10 +105,10 @@ public class LayoutAnimationController {
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;
@@ -132,6 +143,12 @@ public class LayoutAnimationController {
}
if (animation != null) {
long animationDuration = animation.getDuration();
if (animationDuration > mMaxAnimationDuration) {
mMaxAnimationDuration = animationDuration;
scheduleCompletionCallback(animationDuration);
}
view.startAnimation(animation);
}
}
@@ -146,9 +163,7 @@ public class LayoutAnimationController {
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) {
@@ -167,6 +182,12 @@ public class LayoutAnimationController {
}
});
long animationDuration = animation.getDuration();
if (animationDuration > mMaxAnimationDuration) {
scheduleCompletionCallback(animationDuration);
mMaxAnimationDuration = animationDuration;
}
view.startAnimation(animation);
} else {
listener.onAnimationEnd();
@@ -185,4 +206,15 @@ public class LayoutAnimationController {
}
}
}
private void scheduleCompletionCallback(long delayMillis) {
if (sCompletionHandler == null) {
sCompletionHandler = new Handler(Looper.getMainLooper());
}
if (mCompletionRunnable != null) {
sCompletionHandler.removeCallbacks(mCompletionRunnable);
sCompletionHandler.postDelayed(mCompletionRunnable, delayMillis);
}
}
}