diff --git a/Examples/UIExplorer/LayoutAnimationExample.js b/Examples/UIExplorer/LayoutAnimationExample.js
new file mode 100644
index 000000000000..f2ac7b350871
--- /dev/null
+++ b/Examples/UIExplorer/LayoutAnimationExample.js
@@ -0,0 +1,171 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 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.
+ *
+ * @flow
+ */
+'use strict';
+
+const React = require('react');
+const ReactNative = require('react-native');
+const {
+ LayoutAnimation,
+ StyleSheet,
+ Text,
+ View,
+ TouchableOpacity,
+} = ReactNative;
+
+const AddRemoveExample = React.createClass({
+
+ getInitialState() {
+ return {
+ views: [],
+ };
+ },
+
+ componentWillUpdate() {
+ LayoutAnimation.easeInEaseOut();
+ },
+
+ _onPressAddView() {
+ this.setState((state) => ({views: [...state.views, {}]}));
+ },
+
+ _onPressRemoveView() {
+ this.setState((state) => ({views: state.views.slice(0, -1)}));
+ },
+
+ render() {
+ const views = this.state.views.map((view, i) =>
+
+ {i}
+
+ );
+ return (
+
+
+
+ Add view
+
+
+
+
+ Remove view
+
+
+
+ {views}
+
+
+ );
+ },
+});
+
+const GreenSquare = () =>
+
+ Green square
+ ;
+
+const BlueSquare = () =>
+
+ Blue square
+ ;
+
+const CrossFadeExample = React.createClass({
+
+ getInitialState() {
+ return {
+ toggled: false,
+ };
+ },
+
+ _onPressToggle() {
+ LayoutAnimation.easeInEaseOut();
+ this.setState((state) => ({toggled: !state.toggled}));
+ },
+
+ render() {
+ return (
+
+
+
+ Toggle
+
+
+
+ {
+ this.state.toggled ?
+ :
+
+ }
+
+
+ );
+ },
+});
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ button: {
+ borderRadius: 5,
+ backgroundColor: '#eeeeee',
+ padding: 10,
+ marginBottom: 10,
+ },
+ buttonText: {
+ fontSize: 16,
+ },
+ viewContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ view: {
+ height: 54,
+ width: 54,
+ backgroundColor: 'red',
+ margin: 8,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ greenSquare: {
+ width: 150,
+ height: 150,
+ backgroundColor: 'green',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ blueSquare: {
+ width: 150,
+ height: 150,
+ backgroundColor: 'blue',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+exports.title = 'Layout Animation';
+exports.description = 'Layout animation';
+exports.examples = [{
+ title: 'Add and remove views',
+ render(): ReactElement {
+ return ;
+ },
+}, {
+ title: 'Cross fade views',
+ render(): ReactElement {
+ return ;
+ },
+}];
diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js
index 5e8690db2508..17e8b99f9453 100644
--- a/Examples/UIExplorer/UIExplorerList.android.js
+++ b/Examples/UIExplorer/UIExplorerList.android.js
@@ -138,6 +138,10 @@ const APIExamples = [
key: 'LinkingExample',
module: require('./LinkingExample'),
},
+ {
+ key: 'LayoutAnimationExample',
+ module: require('./LayoutAnimationExample'),
+ },
{
key: 'LayoutExample',
module: require('./LayoutExample'),
diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js
index 95768877d5c5..2f03b91d5211 100644
--- a/Examples/UIExplorer/UIExplorerList.ios.js
+++ b/Examples/UIExplorer/UIExplorerList.ios.js
@@ -192,6 +192,10 @@ var APIExamples: Array = [
key: 'ImageEditingExample',
module: require('./ImageEditingExample'),
},
+ {
+ key: 'LayoutAnimationExample',
+ module: require('./LayoutAnimationExample'),
+ },
{
key: 'LayoutExample',
module: require('./LayoutExample'),
diff --git a/Libraries/LayoutAnimation/LayoutAnimation.js b/Libraries/LayoutAnimation/LayoutAnimation.js
index 108c4d5428e5..8a8f221ce026 100644
--- a/Libraries/LayoutAnimation/LayoutAnimation.js
+++ b/Libraries/LayoutAnimation/LayoutAnimation.js
@@ -86,6 +86,10 @@ function create(duration: number, type, creationProp): Config {
update: {
type,
},
+ delete: {
+ type,
+ property: creationProp,
+ },
};
}
@@ -106,6 +110,10 @@ var Presets = {
type: Types.spring,
springDamping: 0.4,
},
+ delete: {
+ type: Types.linear,
+ property: Properties.opacity,
+ },
},
};
diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m
index 51470a2368a8..283c1b55ec95 100644
--- a/React/Modules/RCTUIManager.m
+++ b/React/Modules/RCTUIManager.m
@@ -191,7 +191,6 @@ @implementation RCTUIManager
NSMutableArray *_pendingUIBlocks;
// Animation
- RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only
RCTLayoutAnimation *_layoutAnimation; // Main thread only
NSMutableDictionary *_shadowViewRegistry; // RCT thread only
@@ -582,11 +581,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *
// Perform layout (possibly animated)
return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
- RCTResponseSenderBlock callback = self->_layoutAnimation.callback;
-
- // It's unsafe to call this callback more than once, so we nil it out here
- // to make sure that doesn't happen.
- _layoutAnimation.callback = nil;
+ RCTLayoutAnimation *layoutAnimation = _layoutAnimation;
__block NSUInteger completionsCalled = 0;
for (NSUInteger ii = 0; ii < frames.count; ii++) {
@@ -595,14 +590,18 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *
CGRect frame = [frames[ii] CGRectValue];
BOOL isNew = [areNew[ii] boolValue];
- RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation;
+ RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation;
BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue];
- RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil;
+ RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil;
void (^completion)(BOOL) = ^(BOOL finished) {
completionsCalled++;
- if (callback && completionsCalled == frames.count) {
- callback(@[@(finished)]);
+ if (layoutAnimation.callback && completionsCalled == frames.count) {
+ layoutAnimation.callback(@[@(finished)]);
+
+ // It's unsafe to call this callback more than once, so we nil it out here
+ // to make sure that doesn't happen.
+ layoutAnimation.callback = nil;
}
};
@@ -656,6 +655,8 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *
completion(YES);
}
}
+
+ _layoutAnimation = nil;
};
}
@@ -729,9 +730,49 @@ - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowV
- (void)_removeChildren:(NSArray> *)children
fromContainer:(id)container
+ permanent: (BOOL)permanent
{
+ RCTLayoutAnimation *layoutAnimation = _layoutAnimation;
+ RCTAnimation *deleteAnimation = layoutAnimation.deleteAnimation;
+
+ __block NSUInteger completionsCalled = 0;
+
for (id removedChild in children) {
- [container removeReactSubview:removedChild];
+
+ void (^completion)(BOOL) = ^(BOOL finished) {
+ completionsCalled++;
+
+ [container removeReactSubview:removedChild];
+
+ if (layoutAnimation.callback && completionsCalled == children.count) {
+ layoutAnimation.callback(@[@(finished)]);
+
+ // It's unsafe to call this callback more than once, so we nil it out here
+ // to make sure that doesn't happen.
+ layoutAnimation.callback = nil;
+ }
+ };
+
+ if (permanent && deleteAnimation && [removedChild isKindOfClass: [UIView class]]) {
+ UIView *view = (UIView *)removedChild;
+
+ // Disable user interaction while the view is animating since JS won't receive
+ // the view events anyway.
+ view.userInteractionEnabled = NO;
+
+ [deleteAnimation performAnimations:^{
+ if ([deleteAnimation.property isEqual:@"scaleXY"]) {
+ view.layer.transform = CATransform3DMakeScale(0, 0, 0);
+ } else if ([deleteAnimation.property isEqual:@"opacity"]) {
+ view.layer.opacity = 0.0;
+ } else {
+ RCTLogError(@"Unsupported layout animation createConfig property %@",
+ deleteAnimation.property);
+ }
+ } withCompletionBlock:completion];
+ } else {
+ [container removeReactSubview:removedChild];
+ }
}
}
@@ -848,8 +889,8 @@ - (void)_manageChildren:(NSNumber *)containerReactTag
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
NSArray> *temporarilyRemovedChildren =
[self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
- [self _removeChildren:permanentlyRemovedChildren fromContainer:container];
- [self _removeChildren:temporarilyRemovedChildren fromContainer:container];
+ [self _removeChildren:permanentlyRemovedChildren fromContainer:container permanent:true];
+ [self _removeChildren:temporarilyRemovedChildren fromContainer:container permanent:false];
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
@@ -1015,14 +1056,6 @@ - (void)_layoutAndMount
[self addUIBlock:uiBlock];
}
- // Set up next layout animation
- if (_nextLayoutAnimation) {
- RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation;
- [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) {
- uiManager->_layoutAnimation = layoutAnimation;
- }];
- }
-
// Perform layout
for (NSNumber *reactTag in _rootViewTags) {
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
@@ -1030,14 +1063,6 @@ - (void)_layoutAndMount
[self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:rootView];
}
- // Clear layout animations
- if (_nextLayoutAnimation) {
- [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) {
- uiManager->_layoutAnimation = nil;
- }];
- _nextLayoutAnimation = nil;
- }
-
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) {
/**
* TODO(tadeu): Remove it once and for all
@@ -1429,14 +1454,19 @@ static void RCTMeasureLayout(RCTShadowView *view,
withCallback:(RCTResponseSenderBlock)callback
errorCallback:(__unused RCTResponseSenderBlock)errorCallback)
{
- if (_nextLayoutAnimation && ![config isEqualToDictionary:_nextLayoutAnimation.config]) {
- RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation.config, config);
- }
- if (config[@"delete"] != nil) {
- RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config);
+ RCTLayoutAnimation *currentAnimation = _layoutAnimation;
+
+ if (currentAnimation && ![config isEqualToDictionary:currentAnimation.config]) {
+ RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", currentAnimation.config, config);
}
- _nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config
+
+ RCTLayoutAnimation *nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config
callback:callback];
+
+ // Set up next layout animation
+ [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) {
+ uiManager->_layoutAnimation = nextLayoutAnimation;
+ }];
}
static UIView *_jsResponder;