From f571c62ddf11e5db8346ae50428e476a90b42e9d Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 3 Apr 2019 04:38:50 -0700 Subject: [PATCH] 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 --- Libraries/LayoutAnimation/LayoutAnimation.js | 4 +- RNTester/js/LayoutAnimationExample.js | 21 ++++--- .../uimanager/NativeViewHierarchyManager.java | 4 +- .../react/uimanager/UIImplementation.java | 7 +-- .../react/uimanager/UIManagerModule.java | 5 +- .../react/uimanager/UIViewOperationQueue.java | 11 ++-- .../LayoutAnimationController.java | 56 +++++++++++++++---- 7 files changed, 71 insertions(+), 37 deletions(-) diff --git a/Libraries/LayoutAnimation/LayoutAnimation.js b/Libraries/LayoutAnimation/LayoutAnimation.js index 0b84dd5fe1cf20..ef92d99cec6319 100644 --- a/Libraries/LayoutAnimation/LayoutAnimation.js +++ b/Libraries/LayoutAnimation/LayoutAnimation.js @@ -47,9 +47,7 @@ function configureNext( UIManager.configureNextLayoutAnimation( config, onAnimationDidEnd ?? function() {}, - function() { - /* unused */ - }, + function() {} /* unused onError */, ); } } diff --git a/RNTester/js/LayoutAnimationExample.js b/RNTester/js/LayoutAnimationExample.js index 184c3d5b29cfaa..fb8aac62264561 100644 --- a/RNTester/js/LayoutAnimationExample.js +++ b/RNTester/js/LayoutAnimationExample.js @@ -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); }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 8cabf9ad6efd8a..f5953bacdc45b5 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -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() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index 24512031ff3dd0..dfa310b76d4e26 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -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) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index ec94a1abec68ff..51a229f73ae72e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -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. * - *

TODO(7728153) : animating view deletion is currently not supported. TODO(7613721) : - * callbacks are not supported, this feature will likely be killed. + *

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 void setLayoutAnimationEnabledExperimental(boolean enabled) { */ @ReactMethod public void configureNextLayoutAnimation(ReadableMap config, Callback success, Callback error) { - mUIImplementation.configureNextLayoutAnimation(config, success, error); + mUIImplementation.configureNextLayoutAnimation(config, success); } /** diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index cf366322ef7088..cf66d2a5c50f40 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -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); } } @@ -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( 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 2f08da20ecae2b..232cb01f9071e5 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 @@ -8,11 +8,14 @@ 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 @@ * 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 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 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) { @@ -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; @@ -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); } } @@ -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) { @@ -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(); @@ -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); + } + } }