Skip to content

Commit

Permalink
Send layout events on shadow queue
Browse files Browse the repository at this point in the history
Summary:
We currently wait until after views have been updated on the main thread before sending layout events. This means that any code that relies on those events to update the UI will lag the atual layout by at least one frame.

This changes the RCTUIManager to send the event immediately after layout has occured on the shadow thread. This noticably improves the respinsiveness of the layout example in UIExplorer, which now updates the dimension labels immediately instead of waiting until after the layout animation has completed.
  • Loading branch information
nicklockwood committed Sep 3, 2015
1 parent f28255e commit aa62a5e
Show file tree
Hide file tree
Showing 3 changed files with 22 additions and 24 deletions.
6 changes: 4 additions & 2 deletions Examples/UIExplorer/LayoutEventsExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ var LayoutEventExample = React.createClass({
return (
<View style={this.state.containerStyle}>
<Text>
onLayout events are called on mount and whenever layout is updated,
including after layout animations complete.{' '}
layout events are called on mount and whenever layout is recalculated. Note that the layout event will typically be received <Text style={styles.italicText}>before</Text> the layout has updated on screen, especially when using layout animations.{' '}
<Text style={styles.pressText} onPress={this.animateViewLayout}>
Press here to change layout.
</Text>
Expand Down Expand Up @@ -136,6 +135,9 @@ var styles = StyleSheet.create({
pressText: {
fontWeight: 'bold',
},
italicText: {
fontStyle: 'italic',
},
});

exports.title = 'Layout Events';
Expand Down
4 changes: 4 additions & 0 deletions Libraries/Components/View/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ var View = React.createClass({
* Invoked on mount and layout changes with
*
* {nativeEvent: { layout: {x, y, width, height}}}.
*
* This event is fired immediately once the layout has been calculated, but
* the new layout may not yet be reflected on the screen at the time the
* event is received, especially if a layout animation is in progress.
*/
onLayout: PropTypes.func,

Expand Down
36 changes: 14 additions & 22 deletions React/Modules/RCTUIManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -448,29 +448,12 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];

for (RCTShadowView *shadowView in viewsWithNewFrames) {
[frameReactTags addObject:shadowView.reactTag];
[frames addObject:[NSValue valueWithCGRect:shadowView.frame]];
[areNew addObject:@(shadowView.isNewView)];
[parentsAreNew addObject:@(shadowView.superview.isNewView)];

// TODO (#8214142): this can be greatly simplified by sending the layout
// event directly from the shadow thread, which may be better anyway.
id event = (id)kCFNull;
if (shadowView.onLayout) {
event = @{
@"target": shadowView.reactTag,
@"layout": @{
@"x": @(shadowView.frame.origin.x),
@"y": @(shadowView.frame.origin.y),
@"width": @(shadowView.frame.size.width),
@"height": @(shadowView.frame.size.height),
},
};
}
[onLayoutEvents addObject:event];
}

for (RCTShadowView *shadowView in viewsWithNewFrames) {
Expand All @@ -486,7 +469,20 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
for (RCTShadowView *shadowView in viewsWithNewFrames) {
RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager];
RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView];
if (block) [updateBlocks addObject:block];
if (shadowView.onLayout) {
CGRect frame = shadowView.frame;
shadowView.onLayout(@{
@"layout": @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height),
},
});
}
if (block) {
[updateBlocks addObject:block];
}
}

// Perform layout (possibly animated)
Expand All @@ -497,7 +493,6 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo
NSNumber *reactTag = frameReactTags[ii];
UIView *view = viewRegistry[reactTag];
CGRect frame = [frames[ii] CGRectValue];
id event = onLayoutEvents[ii];

BOOL isNew = [areNew[ii] boolValue];
RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation;
Expand All @@ -506,9 +501,6 @@ - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)roo

void (^completion)(BOOL) = ^(BOOL finished) {
completionsCalled++;
if (event != (id)kCFNull) {
[self.bridge.eventDispatcher sendInputEventWithName:@"layout" body:event];
}
if (callback && completionsCalled == frames.count - 1) {
callback(@[@(finished)]);
}
Expand Down

0 comments on commit aa62a5e

Please sign in to comment.