Permalink
Browse files

LayoutAnimation support for Android RN

Reviewed By: dernienl

Differential Revision: D2710141

fb-gh-sync-id: 28d6af84441b7c2dbc423b73eb05e71f62f7cdea
  • Loading branch information...
Olivier Notteghem facebook-github-bot-4
Olivier Notteghem authored and facebook-github-bot-4 committed Dec 2, 2015
1 parent cf892a9 commit 098fcb3a277f0c363e00aeeb49880db52ab03424
Showing with 698 additions and 5 deletions.
  1. +10 −0 Examples/UIExplorer/ListViewPagingExample.js
  2. +27 −1 ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java
  3. +35 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java
  4. +31 −3 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java
  5. +42 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java
  6. +108 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AbstractLayoutAnimation.java
  7. +32 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/AnimatedPropertyType.java
  8. +48 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/BaseLayoutAnimation.java
  9. +10 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/HandlesLayout.java
  10. +34 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/InterpolatorType.java
  11. +94 −0 ...Android/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationController.java
  12. +22 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutAnimationType.java
  13. +15 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutCreateAnimation.java
  14. +42 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/LayoutUpdateAnimation.java
  15. +66 −0 ReactAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/OpacityAnimation.java
  16. +52 −0 ...tAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/PositionAndSizeAnimation.java
  17. +20 −0 ...tAndroid/src/main/java/com/facebook/react/uimanager/layoutanimation/SimpleSpringInterpolator.java
  18. +10 −1 ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java
@@ -11,6 +11,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
+ * @provides ListViewPagingExample
* @flow
*/
'use strict';
@@ -26,6 +27,11 @@ var {
View,
} = React;
+var NativeModules = require('NativeModules');
+var {
+ UIManager,
+} = NativeModules;
+
var PAGE_SIZE = 4;
var THUMB_URLS = [
'Thumbnails/like.png',
@@ -48,6 +54,10 @@ var Thumb = React.createClass({
getInitialState: function() {
return {thumbIndex: this._getThumbIdx(), dir: 'row'};
},
+ componentWillMount: function() {
+ UIManager.setLayoutAnimationEnabledExperimental &&
+ UIManager.setLayoutAnimationEnabledExperimental(true);
+ },
_getThumbIdx: function() {
return Math.floor(Math.random() * THUMB_URLS.length);
},
@@ -29,9 +29,11 @@
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.JSResponderHandler;
+import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController;
/**
* Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
@@ -65,6 +67,9 @@
private final ViewManagerRegistry mViewManagers;
private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
private final RootViewManager mRootViewManager = new RootViewManager();
+ private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController();
+
+ private boolean mLayoutAnimationEnabled;
public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
mAnimationRegistry = new AnimationRegistry();
@@ -95,6 +100,10 @@ public AnimationRegistry getAnimationRegistry() {
return mAnimationRegistry;
}
+ public void setLayoutAnimationEnabled(boolean enabled) {
+ mLayoutAnimationEnabled = enabled;
+ }
+
public void updateProperties(int tag, CatalystStylesDiffMap props) {
UiThreadUtil.assertOnUiThread();
@@ -149,8 +158,17 @@ public void updateLayout(
}
if (parentViewGroupManager != null
&& !parentViewGroupManager.needsCustomLayoutForChildren()) {
- viewToUpdate.layout(x, y, x + width, y + height);
+ updateLayout(viewToUpdate, x, y, width, height);
}
+ } else {
+ updateLayout(viewToUpdate, x, y, width, height);
+ }
+ }
+
+ private void updateLayout(View viewToUpdate, int x, int y, int width, int height) {
+ if (mLayoutAnimationEnabled &&
+ mLayoutAnimator.shouldAnimateLayout(viewToUpdate)) {
+ mLayoutAnimator.applyLayoutUpdate(viewToUpdate, x, y, width, height);
} else {
viewToUpdate.layout(x, y, x + width, y + height);
}
@@ -466,6 +484,14 @@ public void clearJSResponder() {
mJSResponderHandler.clearJSResponder();
}
+ void configureLayoutAnimation(final ReadableMap config) {
+ mLayoutAnimator.initializeFromConfig(config);
+ }
+
+ void clearLayoutAnimation() {
+ mLayoutAnimator.reset();
+ }
+
/* package */ void startAnimationForNativeView(
int reactTag,
Animation animation,
@@ -469,6 +469,41 @@ public void removeAnimation(int reactTag, int animationID) {
mOperationsQueue.enqueueRemoveAnimation(animationID);
}
+ /**
+ * LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
+ * explicitly in order to avoid regression in existing application written for iOS using this API.
+ *
+ * Warning : This method will be removed in future version of React Native, and layout animation
+ * will be enabled by default, so always check for its existence before invoking it.
+ *
+ * TODO(9139831) : remove this method once layout animation is fully stable.
+ *
+ * @param enabled whether layout animation is enabled or not
+ */
+ public void setLayoutAnimationEnabledExperimental(boolean enabled) {
+ mOperationsQueue.enqueueSetLayoutAnimationEnabled(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.
+ *
+ * @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
+ * 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 setJSResponder(int reactTag, boolean blockNativeResponder) {
assertViewExists(reactTag, "setJSResponder");
ReactShadowNode node = mShadowNodeRegistry.getNode(reactTag);
@@ -374,12 +374,40 @@ public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Cal
mUIImplementation.showPopupMenu(reactTag, items, error, success);
}
+ /**
+ * LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
+ * explicitly in order to avoid regression in existing application written for iOS using this API.
+ *
+ * Warning : This method will be removed in future version of React Native, and layout animation
+ * will be enabled by default, so always check for its existence before invoking it.
+ *
+ * TODO(9139831) : remove this method once layout animation is fully stable.
+ *
+ * @param enabled whether layout animation is enabled or not
+ */
+ @ReactMethod
+ public void setLayoutAnimationEnabledExperimental(boolean enabled) {
+ mUIImplementation.setLayoutAnimationEnabledExperimental(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.
+ *
+ * @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
+ * interrupted. In this case, callback parameter will be false.
+ * @param error will be called if there was an error processing the animation
+ */
@ReactMethod
public void configureNextLayoutAnimation(
ReadableMap config,
- Callback successCallback,
- Callback errorCallback) {
- // TODO(6588266): Implement if required
+ Callback success,
+ Callback error) {
+ mUIImplementation.configureNextLayoutAnimation(config, success, error);
}
/**
@@ -23,6 +23,7 @@
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
import com.facebook.systrace.Systrace;
@@ -322,6 +323,32 @@ public void execute() {
}
}
+ private class SetLayoutAnimationEnabledOperation implements UIOperation {
+ private final boolean mEnabled;
+
+ private SetLayoutAnimationEnabledOperation(final boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.setLayoutAnimationEnabled(mEnabled);
+ }
+ }
+
+ private class ConfigureLayoutAnimationOperation implements UIOperation {
+ private final ReadableMap mConfig;
+
+ private ConfigureLayoutAnimationOperation(final ReadableMap config) {
+ mConfig = config;
+ }
+
+ @Override
+ public void execute() {
+ mNativeViewHierarchyManager.configureLayoutAnimation(mConfig);
+ }
+ }
+
private final class MeasureOperation implements UIOperation {
private final int mReactTag;
@@ -584,6 +611,18 @@ public void enqueueRemoveAnimation(int animationID) {
mOperations.add(new RemoveAnimationOperation(animationID));
}
+ public void enqueueSetLayoutAnimationEnabled(
+ final boolean enabled) {
+ mOperations.add(new SetLayoutAnimationEnabledOperation(enabled));
+ }
+
+ public void enqueueConfigureLayoutAnimation(
+ final ReadableMap config,
+ final Callback onSuccess,
+ final Callback onError) {
+ mOperations.add(new ConfigureLayoutAnimationOperation(config));
+ }
+
public void enqueueMeasure(
final int reactTag,
final Callback callback) {
@@ -680,6 +719,9 @@ public void doFrameGuarded(long frameTimeNanos) {
mDispatchUIRunnables.get(i).run();
}
mDispatchUIRunnables.clear();
+
+ // Clear layout animation, as animation only apply to current UI operations batch.
+ mNativeViewHierarchyManager.clearLayoutAnimation();
}
ReactChoreographer.getInstance().postFrameCallback(
@@ -0,0 +1,108 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.uimanager.layoutanimation;
+
+import javax.annotation.Nullable;
+
+import java.util.Map;
+
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.common.MapBuilder;
+import com.facebook.react.uimanager.IllegalViewOperationException;
+
+/**
+ * Class responsible for parsing and converting layout animation data into native {@link Animation}
+ * in order to animate layout when a valid configuration has been supplied by the application.
+ */
+/* package */ abstract class AbstractLayoutAnimation {
+
+ // Forces animation to be playing 10x slower, used for debug purposes.
+ private static final boolean SLOWDOWN_ANIMATION_MODE = false;
+
+ abstract boolean isValid();
+
+ /**
+ * Create an animation object for the current animation type, based on the view and final screen
+ * coordinates. If the application-supplied configuraiton does not specify an animation definition
+ * for this types, or if the animation definition is invalid, returns null.
+ */
+ abstract @Nullable Animation createAnimationImpl(View view, int x, int y, int width, int height);
+
+ private static final Map<InterpolatorType, Interpolator> INTERPOLATOR = MapBuilder.of(
+ InterpolatorType.LINEAR, new LinearInterpolator(),
+ InterpolatorType.EASE_IN, new AccelerateInterpolator(),
+ InterpolatorType.EASE_OUT, new DecelerateInterpolator(),
+ InterpolatorType.EASE_IN_EASE_OUT, new AccelerateDecelerateInterpolator(),
+ InterpolatorType.SPRING, new SimpleSpringInterpolator());
+
+ private @Nullable Interpolator mInterpolator;
+ private int mDelayMs;
+
+ protected @Nullable AnimatedPropertyType mAnimatedProperty;
+ protected int mDurationMs;
+
+ public void reset() {
+ mAnimatedProperty = null;
+ mDurationMs = 0;
+ mDelayMs = 0;
+ mInterpolator = null;
+ }
+
+ public void initializeFromConfig(ReadableMap data, int globalDuration) {
+ mAnimatedProperty = data.hasKey("property") ?
+ AnimatedPropertyType.fromString(data.getString("property")) : null;
+ mDurationMs = data.hasKey("duration") ? data.getInt("duration") : globalDuration;
+ mDelayMs = data.hasKey("delay") ? data.getInt("delay") : 0;
+ mInterpolator = data.hasKey("type") ?
+ getInterpolator(InterpolatorType.fromString(data.getString("type"))) : null;
+
+ if (!isValid()) {
+ throw new IllegalViewOperationException("Invalid layout animation : " + data);
+ }
+ }
+
+ /**
+ * Create an animation object to be used to animate the view, based on the animation config
+ * supplied at initialization time and the new view position and size.
+ *
+ * @param view the view to create the animation for
+ * @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
+ */
+ public final @Nullable Animation createAnimation(
+ View view,
+ int x,
+ int y,
+ int width,
+ int height) {
+ if (!isValid()) {
+ return null;
+ }
+ Animation animation = createAnimationImpl(view, x, y, width, height);
+ if (animation != null) {
+ int slowdownFactor = SLOWDOWN_ANIMATION_MODE ? 10 : 1;
+ animation.setDuration(mDurationMs * slowdownFactor);
+ animation.setStartOffset(mDelayMs * slowdownFactor);
+ animation.setInterpolator(mInterpolator);
+ }
+ return animation;
+ }
+
+ private static Interpolator getInterpolator(InterpolatorType type) {
+ Interpolator interpolator = INTERPOLATOR.get(type);
+ if (interpolator == null) {
+ throw new IllegalArgumentException("Missing interpolator for type : " + type);
+ }
+ return interpolator;
+ }
+}
@@ -0,0 +1,32 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+package com.facebook.react.uimanager.layoutanimation;
+
+/**
+ * Enum representing the different view properties that can be used when animating layout for
+ * view creation.
+ */
+/* package */ enum AnimatedPropertyType {
+ OPACITY("opacity"),
+ SCALE_XY("scaleXY");
+
+ private final String mName;
+
+ private AnimatedPropertyType(String name) {
+ mName = name;
+ }
+
+ public static AnimatedPropertyType fromString(String name) {
+ for (AnimatedPropertyType property : AnimatedPropertyType.values()) {
+ if (property.toString().equalsIgnoreCase(name)) {
+ return property;
+ }
+ }
+ throw new IllegalArgumentException("Unsupported animated property : " + name);
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+}
Oops, something went wrong.

0 comments on commit 098fcb3

Please sign in to comment.