Skip to content

Commit 593a45e

Browse files
olinotteghemfacebook-github-bot-9
authored andcommitted
LayoutAnimation support for Android RN
Reviewed By: astreet Differential Revision: D2217731 fb-gh-sync-id: d990af4b630995f95433690d5dcf510382dc34d2
1 parent 4890424 commit 593a45e

17 files changed

+668
-5
lines changed

Examples/UIExplorer/ListViewPagingExample.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
1212
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1313
*
14+
* @provides ListViewPagingExample
1415
* @flow
1516
*/
1617
'use strict';
@@ -26,6 +27,11 @@ var {
2627
View,
2728
} = React;
2829

30+
var NativeModules = require('NativeModules');
31+
var {
32+
UIManager,
33+
} = NativeModules;
34+
2935
var PAGE_SIZE = 4;
3036
var THUMB_URLS = [
3137
'Thumbnails/like.png',
@@ -48,6 +54,10 @@ var Thumb = React.createClass({
4854
getInitialState: function() {
4955
return {thumbIndex: this._getThumbIdx(), dir: 'row'};
5056
},
57+
componentWillMount: function() {
58+
UIManager.setLayoutAnimationEnabledExperimental &&
59+
UIManager.setLayoutAnimationEnabledExperimental(true);
60+
},
5161
_getThumbIdx: function() {
5262
return Math.floor(Math.random() * THUMB_URLS.length);
5363
},

ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
3030
import com.facebook.react.bridge.ReactContext;
3131
import com.facebook.react.bridge.ReadableArray;
32+
import com.facebook.react.bridge.ReadableMap;
3233
import com.facebook.react.bridge.SoftAssertions;
3334
import com.facebook.react.bridge.UiThreadUtil;
3435
import com.facebook.react.touch.JSResponderHandler;
36+
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController;
3537

3638
/**
3739
* Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between
@@ -66,6 +68,9 @@
6668
private final ViewManagerRegistry mViewManagers;
6769
private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
6870
private final RootViewManager mRootViewManager = new RootViewManager();
71+
private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController();
72+
73+
private boolean mLayoutAnimationEnabled;
6974

7075
public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) {
7176
mAnimationRegistry = new AnimationRegistry();
@@ -80,6 +85,10 @@ public AnimationRegistry getAnimationRegistry() {
8085
return mAnimationRegistry;
8186
}
8287

88+
public void setLayoutAnimationEnabled(boolean enabled) {
89+
mLayoutAnimationEnabled = enabled;
90+
}
91+
8392
public void updateProperties(int tag, CatalystStylesDiffMap props) {
8493
UiThreadUtil.assertOnUiThread();
8594

@@ -154,8 +163,17 @@ public void updateLayout(
154163
}
155164
if (parentViewGroupManager != null
156165
&& !parentViewGroupManager.needsCustomLayoutForChildren()) {
157-
viewToUpdate.layout(x, y, x + width, y + height);
166+
updateLayout(viewToUpdate, x, y, width, height);
158167
}
168+
} else {
169+
updateLayout(viewToUpdate, x, y, width, height);
170+
}
171+
}
172+
173+
private void updateLayout(View viewToUpdate, int x, int y, int width, int height) {
174+
if (mLayoutAnimationEnabled &&
175+
mLayoutAnimator.shouldAnimateLayout(viewToUpdate)) {
176+
mLayoutAnimator.applyLayoutUpdate(viewToUpdate, x, y, width, height);
159177
} else {
160178
viewToUpdate.layout(x, y, x + width, y + height);
161179
}
@@ -470,6 +488,14 @@ public void clearJSResponder() {
470488
mJSResponderHandler.clearJSResponder();
471489
}
472490

491+
void configureLayoutAnimation(final ReadableMap config) {
492+
mLayoutAnimator.initializeFromConfig(config);
493+
}
494+
495+
void clearLayoutAnimation() {
496+
mLayoutAnimator.reset();
497+
}
498+
473499
/* package */ void startAnimationForNativeView(
474500
int reactTag,
475501
Animation animation,

ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,12 +383,45 @@ public void showPopupMenu(int reactTag, ReadableArray items, Callback error, Cal
383383
mUIImplementation.showPopupMenu(reactTag, items, error, success);
384384
}
385385

386+
@ReactMethod
387+
public void setMainScrollViewTag(int reactTag) {
388+
// TODO(6588266): Implement if required
389+
}
390+
391+
/**
392+
* LayoutAnimation API on Android is currently experimental. Therefore, it needs to be enabled
393+
* explicitly in order to avoid regression in existing application written for iOS using this API.
394+
*
395+
* Warning : This method will be removed in future version of React Native, and layout animation
396+
* will be enabled by default, so always check for its existence before invoking it.
397+
*
398+
* TODO(9139831) : remove this method once layout animation is fully stable.
399+
*
400+
* @param enabled whether layout animation is enabled or not
401+
*/
402+
@ReactMethod
403+
public void setLayoutAnimationEnabledExperimental(boolean enabled) {
404+
mOperationsQueue.enqueueSetLayoutAnimationEnabled(enabled);
405+
}
406+
407+
/**
408+
* Configure an animation to be used for the native layout changes, and native views
409+
* creation. The animation will only apply during the current batch operations.
410+
*
411+
* TODO(7728153) : animating view deletion is currently not supported.
412+
* TODO(7613721) : callbacks are not supported, this feature will likely be killed.
413+
*
414+
* @param config the configuration of the animation for view addition/removal/update.
415+
* @param success will be called when the animation completes, or when the animation get
416+
* interrupted. In this case, callback parameter will be false.
417+
* @param error will be called if there was an error processing the animation
418+
*/
386419
@ReactMethod
387420
public void configureNextLayoutAnimation(
388421
ReadableMap config,
389-
Callback successCallback,
390-
Callback errorCallback) {
391-
// TODO(6588266): Implement if required
422+
Callback success,
423+
Callback error) {
424+
mOperationsQueue.enqueueConfigureLayoutAnimation(config, success, error);
392425
}
393426

394427
/**

ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.facebook.react.bridge.SoftAssertions;
2424
import com.facebook.react.bridge.ReactContext;
2525
import com.facebook.react.bridge.ReadableArray;
26+
import com.facebook.react.bridge.ReadableMap;
2627
import com.facebook.react.bridge.UiThreadUtil;
2728
import com.facebook.react.uimanager.debug.NotThreadSafeViewHierarchyUpdateDebugListener;
2829
import com.facebook.systrace.Systrace;
@@ -322,6 +323,32 @@ public void execute() {
322323
}
323324
}
324325

326+
private class SetLayoutAnimationEnabledOperation implements UIOperation {
327+
private final boolean mEnabled;
328+
329+
private SetLayoutAnimationEnabledOperation(final boolean enabled) {
330+
mEnabled = enabled;
331+
}
332+
333+
@Override
334+
public void execute() {
335+
mNativeViewHierarchyManager.setLayoutAnimationEnabled(mEnabled);
336+
}
337+
}
338+
339+
private class ConfigureLayoutAnimationOperation implements UIOperation {
340+
private final ReadableMap mConfig;
341+
342+
private ConfigureLayoutAnimationOperation(final ReadableMap config) {
343+
mConfig = config;
344+
}
345+
346+
@Override
347+
public void execute() {
348+
mNativeViewHierarchyManager.configureLayoutAnimation(mConfig);
349+
}
350+
}
351+
325352
private final class MeasureOperation implements UIOperation {
326353

327354
private final int mReactTag;
@@ -576,6 +603,18 @@ public void enqueueRemoveAnimation(int animationID) {
576603
mOperations.add(new RemoveAnimationOperation(animationID));
577604
}
578605

606+
public void enqueueSetLayoutAnimationEnabled(
607+
final boolean enabled) {
608+
mOperations.add(new SetLayoutAnimationEnabledOperation(enabled));
609+
}
610+
611+
public void enqueueConfigureLayoutAnimation(
612+
final ReadableMap config,
613+
final Callback onSuccess,
614+
final Callback onError) {
615+
mOperations.add(new ConfigureLayoutAnimationOperation(config));
616+
}
617+
579618
public void enqueueMeasure(
580619
final int reactTag,
581620
final Callback callback) {
@@ -672,6 +711,9 @@ public void doFrameGuarded(long frameTimeNanos) {
672711
mDispatchUIRunnables.get(i).run();
673712
}
674713
mDispatchUIRunnables.clear();
714+
715+
// Clear layout animation, as animation only apply to current UI operations batch.
716+
mNativeViewHierarchyManager.clearLayoutAnimation();
675717
}
676718

677719
ReactChoreographer.getInstance().postFrameCallback(
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2004-present Facebook. All Rights Reserved.
2+
3+
package com.facebook.react.uimanager.layoutanimation;
4+
5+
import javax.annotation.Nullable;
6+
7+
import java.util.Map;
8+
9+
import android.view.View;
10+
import android.view.animation.AccelerateDecelerateInterpolator;
11+
import android.view.animation.AccelerateInterpolator;
12+
import android.view.animation.Animation;
13+
import android.view.animation.DecelerateInterpolator;
14+
import android.view.animation.Interpolator;
15+
import android.view.animation.LinearInterpolator;
16+
17+
import com.facebook.react.bridge.ReadableMap;
18+
import com.facebook.react.common.MapBuilder;
19+
import com.facebook.react.uimanager.IllegalViewOperationException;
20+
21+
/**
22+
* Class responsible for parsing and converting layout animation data into native {@link Animation}
23+
* in order to animate layout when a valid configuration has been supplied by the application.
24+
*/
25+
/* package */ abstract class AbstractLayoutAnimation {
26+
27+
// Forces animation to be playing 10x slower, used for debug purposes.
28+
private static final boolean SLOWDOWN_ANIMATION_MODE = false;
29+
30+
abstract boolean isValid();
31+
32+
/**
33+
* Create an animation object for the current animation type, based on the view and final screen
34+
* coordinates. If the application-supplied configuraiton does not specify an animation definition
35+
* for this types, or if the animation definition is invalid, returns null.
36+
*/
37+
abstract @Nullable Animation createAnimationImpl(View view, int x, int y, int width, int height);
38+
39+
private static final Map<InterpolatorType, Interpolator> INTERPOLATOR = MapBuilder.of(
40+
InterpolatorType.LINEAR, new LinearInterpolator(),
41+
InterpolatorType.EASE_IN, new AccelerateInterpolator(),
42+
InterpolatorType.EASE_OUT, new DecelerateInterpolator(),
43+
InterpolatorType.EASE_IN_EASE_OUT, new AccelerateDecelerateInterpolator(),
44+
InterpolatorType.SPRING, new SimpleSpringInterpolator());
45+
46+
private @Nullable Interpolator mInterpolator;
47+
private int mDelayMs;
48+
49+
protected @Nullable AnimatedPropertyType mAnimatedProperty;
50+
protected int mDurationMs;
51+
52+
public void reset() {
53+
mAnimatedProperty = null;
54+
mDurationMs = 0;
55+
mDelayMs = 0;
56+
mInterpolator = null;
57+
}
58+
59+
public void initializeFromConfig(ReadableMap data, int globalDuration) {
60+
mAnimatedProperty = data.hasKey("property") ?
61+
AnimatedPropertyType.fromString(data.getString("property")) : null;
62+
mDurationMs = data.hasKey("duration") ? data.getInt("duration") : globalDuration;
63+
mDelayMs = data.hasKey("delay") ? data.getInt("delay") : 0;
64+
mInterpolator = data.hasKey("type") ?
65+
getInterpolator(InterpolatorType.fromString(data.getString("type"))) : null;
66+
67+
if (!isValid()) {
68+
throw new IllegalViewOperationException("Invalid layout animation : " + data);
69+
}
70+
}
71+
72+
/**
73+
* Create an animation object to be used to animate the view, based on the animation config
74+
* supplied at initialization time and the new view position and size.
75+
*
76+
* @param view the view to create the animation for
77+
* @param x the new X position for the view
78+
* @param y the new Y position for the view
79+
* @param width the new width value for the view
80+
* @param height the new height value for the view
81+
*/
82+
public final @Nullable Animation createAnimation(
83+
View view,
84+
int x,
85+
int y,
86+
int width,
87+
int height) {
88+
if (!isValid()) {
89+
return null;
90+
}
91+
Animation animation = createAnimationImpl(view, x, y, width, height);
92+
if (animation != null) {
93+
int slowdownFactor = SLOWDOWN_ANIMATION_MODE ? 10 : 1;
94+
animation.setDuration(mDurationMs * slowdownFactor);
95+
animation.setStartOffset(mDelayMs * slowdownFactor);
96+
animation.setInterpolator(mInterpolator);
97+
}
98+
return animation;
99+
}
100+
101+
private static Interpolator getInterpolator(InterpolatorType type) {
102+
Interpolator interpolator = INTERPOLATOR.get(type);
103+
if (interpolator == null) {
104+
throw new IllegalArgumentException("Missing interpolator for type : " + type);
105+
}
106+
return interpolator;
107+
}
108+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2004-present Facebook. All Rights Reserved.
2+
3+
package com.facebook.react.uimanager.layoutanimation;
4+
5+
/**
6+
* Enum representing the different view properties that can be used when animating layout for
7+
* view creation.
8+
*/
9+
/* package */ enum AnimatedPropertyType {
10+
OPACITY("opacity"),
11+
SCALE_XY("scaleXY");
12+
13+
private final String mName;
14+
15+
private AnimatedPropertyType(String name) {
16+
mName = name;
17+
}
18+
19+
public static AnimatedPropertyType fromString(String name) {
20+
for (AnimatedPropertyType property : AnimatedPropertyType.values()) {
21+
if (property.toString().equalsIgnoreCase(name)) {
22+
return property;
23+
}
24+
}
25+
throw new IllegalArgumentException("Unsupported animated property : " + name);
26+
}
27+
28+
@Override
29+
public String toString() {
30+
return mName;
31+
}
32+
}

0 commit comments

Comments
 (0)