Skip to content

Commit

Permalink
LayoutAnimation support for Android RN
Browse files Browse the repository at this point in the history
Reviewed By: astreet

Differential Revision: D2217731

fb-gh-sync-id: d990af4b630995f95433690d5dcf510382dc34d2
  • Loading branch information
olinotteghem authored and facebook-github-bot-9 committed Nov 30, 2015
1 parent 4890424 commit 593a45e
Show file tree
Hide file tree
Showing 17 changed files with 668 additions and 5 deletions.
10 changes: 10 additions & 0 deletions Examples/UIExplorer/ListViewPagingExample.js
Expand Up @@ -11,6 +11,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* *
* @provides ListViewPagingExample
* @flow * @flow
*/ */
'use strict'; 'use strict';
Expand All @@ -26,6 +27,11 @@ var {
View, View,
} = React; } = React;


var NativeModules = require('NativeModules');
var {
UIManager,
} = NativeModules;

var PAGE_SIZE = 4; var PAGE_SIZE = 4;
var THUMB_URLS = [ var THUMB_URLS = [
'Thumbnails/like.png', 'Thumbnails/like.png',
Expand All @@ -48,6 +54,10 @@ var Thumb = React.createClass({
getInitialState: function() { getInitialState: function() {
return {thumbIndex: this._getThumbIdx(), dir: 'row'}; return {thumbIndex: this._getThumbIdx(), dir: 'row'};
}, },
componentWillMount: function() {
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
},
_getThumbIdx: function() { _getThumbIdx: function() {
return Math.floor(Math.random() * THUMB_URLS.length); return Math.floor(Math.random() * THUMB_URLS.length);
}, },
Expand Down
Expand Up @@ -29,9 +29,11 @@
import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.touch.JSResponderHandler; 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 * Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
Expand Down Expand Up @@ -66,6 +68,9 @@
private final ViewManagerRegistry mViewManagers; private final ViewManagerRegistry mViewManagers;
private final JSResponderHandler mJSResponderHandler = new JSResponderHandler(); private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
private final RootViewManager mRootViewManager = new RootViewManager(); private final RootViewManager mRootViewManager = new RootViewManager();
private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController();

private boolean mLayoutAnimationEnabled;


public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) { public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
mAnimationRegistry = new AnimationRegistry(); mAnimationRegistry = new AnimationRegistry();
Expand All @@ -80,6 +85,10 @@ public AnimationRegistry getAnimationRegistry() {
return mAnimationRegistry; return mAnimationRegistry;
} }


public void setLayoutAnimationEnabled(boolean enabled) {
mLayoutAnimationEnabled = enabled;
}

public void updateProperties(int tag, CatalystStylesDiffMap props) { public void updateProperties(int tag, CatalystStylesDiffMap props) {
UiThreadUtil.assertOnUiThread(); UiThreadUtil.assertOnUiThread();


Expand Down Expand Up @@ -154,8 +163,17 @@ public void updateLayout(
} }
if (parentViewGroupManager != null if (parentViewGroupManager != null
&& !parentViewGroupManager.needsCustomLayoutForChildren()) { && !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 { } else {
viewToUpdate.layout(x, y, x + width, y + height); viewToUpdate.layout(x, y, x + width, y + height);
} }
Expand Down Expand Up @@ -470,6 +488,14 @@ public void clearJSResponder() {
mJSResponderHandler.clearJSResponder(); mJSResponderHandler.clearJSResponder();
} }


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

void clearLayoutAnimation() {
mLayoutAnimator.reset();
}

/* package */ void startAnimationForNativeView( /* package */ void startAnimationForNativeView(
int reactTag, int reactTag,
Animation animation, Animation animation,
Expand Down
Expand Up @@ -383,12 +383,45 @@ public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Cal
mUIImplementation.showPopupMenu(reactTag, items, error, success); mUIImplementation.showPopupMenu(reactTag, items, error, success);
} }


@ReactMethod
public void setMainScrollViewTag(int reactTag) {
// TODO(6588266): Implement if required
}

/**
* 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) {
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
*/
@ReactMethod @ReactMethod
public void configureNextLayoutAnimation( public void configureNextLayoutAnimation(
ReadableMap config, ReadableMap config,
Callback successCallback, Callback success,
Callback errorCallback) { Callback error) {
// TODO(6588266): Implement if required mOperationsQueue.enqueueConfigureLayoutAnimation(config, success, error);
} }


/** /**
Expand Down
Expand Up @@ -23,6 +23,7 @@
import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener; import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
import com.facebook.systrace.Systrace; import com.facebook.systrace.Systrace;
Expand Down Expand Up @@ -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 class MeasureOperation implements UIOperation {


private final int mReactTag; private final int mReactTag;
Expand Down Expand Up @@ -576,6 +603,18 @@ public void enqueueRemoveAnimation(int animationID) {
mOperations.add(new RemoveAnimationOperation(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( public void enqueueMeasure(
final int reactTag, final int reactTag,
final Callback callback) { final Callback callback) {
Expand Down Expand Up @@ -672,6 +711,9 @@ public void doFrameGuarded(long frameTimeNanos) {
mDispatchUIRunnables.get(i).run(); mDispatchUIRunnables.get(i).run();
} }
mDispatchUIRunnables.clear(); mDispatchUIRunnables.clear();

// Clear layout animation, as animation only apply to current UI operations batch.
mNativeViewHierarchyManager.clearLayoutAnimation();
} }


ReactChoreographer.getInstance().postFrameCallback( ReactChoreographer.getInstance().postFrameCallback(
Expand Down
@@ -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;
}
}

0 comments on commit 593a45e

Please sign in to comment.