Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions Examples/UIExplorer/LayoutAnimationExample.js
Original file line number Diff line number Diff line change
@@ -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) =>
<View key={i} style={styles.view}>
<Text>{i}</Text>
</View>
);
return (
<View style={styles.container}>
<TouchableOpacity onPress={this._onPressAddView}>
<View style={styles.button}>
<Text>Add view</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={this._onPressRemoveView}>
<View style={styles.button}>
<Text>Remove view</Text>
</View>
</TouchableOpacity>
<View style={styles.viewContainer}>
{views}
</View>
</View>
);
},
});

const GreenSquare = () =>
<View style={styles.greenSquare}>
<Text>Green square</Text>
</View>;

const BlueSquare = () =>
<View style={styles.blueSquare}>
<Text>Blue square</Text>
</View>;

const CrossFadeExample = React.createClass({

getInitialState() {
return {
toggled: false,
};
},

_onPressToggle() {
LayoutAnimation.easeInEaseOut();
this.setState((state) => ({toggled: !state.toggled}));
},

render() {
return (
<View style={styles.container}>
<TouchableOpacity onPress={this._onPressToggle}>
<View style={styles.button}>
<Text>Toggle</Text>
</View>
</TouchableOpacity>
<View style={styles.viewContainer}>
{
this.state.toggled ?
<GreenSquare /> :
<BlueSquare />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty sick how easy this is.

}
</View>
</View>
);
},
});

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 <AddRemoveExample />;
},
}, {
title: 'Cross fade views',
render(): ReactElement {
return <CrossFadeExample />;
},
}];
4 changes: 4 additions & 0 deletions Examples/UIExplorer/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ const APIExamples = [
key: 'LinkingExample',
module: require('./LinkingExample'),
},
{
key: 'LayoutAnimationExample',
module: require('./LayoutAnimationExample'),
},
{
key: 'LayoutExample',
module: require('./LayoutExample'),
Expand Down
4 changes: 4 additions & 0 deletions Examples/UIExplorer/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ var APIExamples: Array<UIExplorerExample> = [
key: 'ImageEditingExample',
module: require('./ImageEditingExample'),
},
{
key: 'LayoutAnimationExample',
module: require('./LayoutAnimationExample'),
},
{
key: 'LayoutExample',
module: require('./LayoutExample'),
Expand Down
8 changes: 8 additions & 0 deletions Libraries/LayoutAnimation/LayoutAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ function create(duration: number, type, creationProp): Config {
update: {
type,
},
delete: {
type,
property: creationProp,
},
};
}

Expand All @@ -106,6 +110,10 @@ var Presets = {
type: Types.spring,
springDamping: 0.4,
},
delete: {
type: Types.linear,
property: Properties.opacity,
},
},
};

Expand Down
100 changes: 65 additions & 35 deletions React/Modules/RCTUIManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ @implementation RCTUIManager
NSMutableArray<dispatch_block_t> *_pendingUIBlocks;

// Animation
RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only
RCTLayoutAnimation *_layoutAnimation; // Main thread only

NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
Expand Down Expand Up @@ -582,11 +581,7 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *

// Perform layout (possibly animated)
return ^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *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++) {
Expand All @@ -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;
}
};

Expand Down Expand Up @@ -656,6 +655,8 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *
completion(YES);
}
}

_layoutAnimation = nil;
};
}

Expand Down Expand Up @@ -729,9 +730,49 @@ - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowV

- (void)_removeChildren:(NSArray<id<RCTComponent>> *)children
fromContainer:(id<RCTComponent>)container
permanent: (BOOL)permanent
{
RCTLayoutAnimation *layoutAnimation = _layoutAnimation;
RCTAnimation *deleteAnimation = layoutAnimation.deleteAnimation;

__block NSUInteger completionsCalled = 0;

for (id<RCTComponent> 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];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about weird edge-cases that could happen if the user interacts with the app while this deletion animation is running. Perhaps we should make the view immediately non-interactable? Any other safeguards we could use? How much did you bang on the prototype?

} else {
[container removeReactSubview:removedChild];
}
}
}

Expand Down Expand Up @@ -848,8 +889,8 @@ - (void)_manageChildren:(NSNumber *)containerReactTag
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
NSArray<id<RCTComponent>> *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];

Expand Down Expand Up @@ -1015,29 +1056,13 @@ - (void)_layoutAndMount
[self addUIBlock:uiBlock];
}

// Set up next layout animation
if (_nextLayoutAnimation) {
RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation;
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
uiManager->_layoutAnimation = layoutAnimation;
}];
}

// Perform layout
for (NSNumber *reactTag in _rootViewTags) {
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
[self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:rootView];
}

// Clear layout animations
if (_nextLayoutAnimation) {
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
uiManager->_layoutAnimation = nil;
}];
_nextLayoutAnimation = nil;
}

[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
/**
* TODO(tadeu): Remove it once and for all
Expand Down Expand Up @@ -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<NSNumber *, UIView *> *viewRegistry) {
uiManager->_layoutAnimation = nextLayoutAnimation;
}];
}

static UIView *_jsResponder;
Expand Down