Skip to content

Commit 1ef4e00

Browse files
committed
[ReactNative] Introduce onLayout events
Summary: Simply add an `onLayout` callback to a native view component, and the callback will be invoked with the current layout information when the view is mounted and whenever the layout changes. The only limitation is that scroll position and other stuff the layout system isn't aware of is not taken into account. This is because onLayout events wouldn't be triggered for these changes and if they are desired they should be tracked separately (e.g. with `onScroll`) and combined. Also fixes some bugs with LayoutAnimation callbacks. @public Test Plan: - Run new LayoutEventsExample in UIExplorer and see it work correctly. - New integration test passes internally (IntegrationTest project seems busted). - New jest test case passes. {F22318433} ``` 2015-05-06 15:45:05.848 [info][tid:com.facebook.React.JavaScript] "Running application "UIExplorerApp" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF" 2015-05-06 15:45:05.881 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":123,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.882 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":122,"width":50,"height":50}} 2015-05-06 15:45:05.883 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":204}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":287.5}} 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "layout animation done." 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":82,"width":50,"height":50}} 2015-05-06 15:45:09.848 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":110.5,"x":60,"width":214,"height":287.5}} 2015-05-06 15:45:09.862 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":120,"height":68}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":55,"width":50,"height":50}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":128,"x":60,"width":160,"height":337.5}} ```
1 parent f251b0d commit 1ef4e00

File tree

11 files changed

+389
-11
lines changed

11 files changed

+389
-11
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* The examples provided by Facebook are for non-commercial testing and
3+
* evaluation purposes only.
4+
*
5+
* Facebook reserves all rights not expressly granted.
6+
*
7+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
8+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9+
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
10+
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
11+
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
12+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13+
*
14+
* @flow
15+
*/
16+
'use strict';
17+
18+
var React = require('react-native');
19+
var {
20+
Image,
21+
LayoutAnimation,
22+
StyleSheet,
23+
Text,
24+
View,
25+
} = React;
26+
27+
type LayoutEvent = {
28+
nativeEvent: {
29+
layout: {
30+
x: number;
31+
y: number;
32+
width: number;
33+
height: number;
34+
};
35+
};
36+
};
37+
38+
var LayoutEventExample = React.createClass({
39+
getInitialState: function() {
40+
return {
41+
viewStyle: {
42+
margin: 20,
43+
},
44+
};
45+
},
46+
animateViewLayout: function() {
47+
LayoutAnimation.configureNext(
48+
LayoutAnimation.Presets.spring,
49+
() => {
50+
console.log('layout animation done.');
51+
this.addWrapText();
52+
},
53+
(error) => { throw new Error(JSON.stringify(error)); }
54+
);
55+
this.setState({
56+
viewStyle: {
57+
margin: this.state.viewStyle.margin > 20 ? 20 : 60,
58+
}
59+
});
60+
},
61+
addWrapText: function() {
62+
this.setState(
63+
{extraText: ' And a bunch more text to wrap around a few lines.'},
64+
this.changeContainer
65+
);
66+
},
67+
changeContainer: function() {
68+
this.setState({containerStyle: {width: 280}});
69+
},
70+
onViewLayout: function(e: LayoutEvent) {
71+
console.log('received view layout event\n', e.nativeEvent);
72+
this.setState({viewLayout: e.nativeEvent.layout});
73+
},
74+
onTextLayout: function(e: LayoutEvent) {
75+
console.log('received text layout event\n', e.nativeEvent);
76+
this.setState({textLayout: e.nativeEvent.layout});
77+
},
78+
onImageLayout: function(e: LayoutEvent) {
79+
console.log('received image layout event\n', e.nativeEvent);
80+
this.setState({imageLayout: e.nativeEvent.layout});
81+
},
82+
render: function() {
83+
var viewStyle = [styles.view, this.state.viewStyle];
84+
var textLayout = this.state.textLayout || {width: '?', height: '?'};
85+
var imageLayout = this.state.imageLayout || {x: '?', y: '?'};
86+
return (
87+
<View style={this.state.containerStyle}>
88+
<Text>
89+
onLayout events are called on mount and whenever layout is updated,
90+
including after layout animations complete.{' '}
91+
<Text style={styles.pressText} onPress={this.animateViewLayout}>
92+
Press here to change layout.
93+
</Text>
94+
</Text>
95+
<View ref="view" onLayout={this.onViewLayout} style={viewStyle}>
96+
<Image
97+
ref="img"
98+
onLayout={this.onImageLayout}
99+
style={styles.image}
100+
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
101+
/>
102+
<Text>
103+
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
104+
</Text>
105+
<Text ref="txt" onLayout={this.onTextLayout} style={styles.text}>
106+
A simple piece of text.{this.state.extraText}
107+
</Text>
108+
<Text>
109+
{'\n'}
110+
Text w/h: {textLayout.width}/{textLayout.height + '\n'}
111+
Image x/y: {imageLayout.x}/{imageLayout.y}
112+
</Text>
113+
</View>
114+
</View>
115+
);
116+
}
117+
});
118+
119+
var styles = StyleSheet.create({
120+
view: {
121+
padding: 12,
122+
borderColor: 'black',
123+
borderWidth: 0.5,
124+
backgroundColor: 'transparent',
125+
},
126+
text: {
127+
alignSelf: 'flex-start',
128+
borderColor: 'rgba(0, 0, 255, 0.2)',
129+
borderWidth: 0.5,
130+
},
131+
image: {
132+
width: 50,
133+
height: 50,
134+
marginBottom: 10,
135+
alignSelf: 'center',
136+
},
137+
pressText: {
138+
fontWeight: 'bold',
139+
},
140+
});
141+
142+
exports.title = 'onLayout';
143+
exports.description = 'Layout events can be used to measure view size and position.';
144+
exports.examples = [
145+
{
146+
title: 'onLayout',
147+
render: function(): ReactElement {
148+
return <LayoutEventExample />;
149+
},
150+
}];

Examples/UIExplorer/UIExplorerList.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ var APIS = [
6464
require('./BorderExample'),
6565
require('./CameraRollExample.ios'),
6666
require('./GeolocationExample'),
67+
require('./LayoutEventsExample'),
6768
require('./LayoutExample'),
6869
require('./NetInfoExample'),
6970
require('./PanResponderExample'),

IntegrationTests/IntegrationTestsApp.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var TESTS = [
2525
require('./IntegrationTestHarnessTest'),
2626
require('./TimersTest'),
2727
require('./AsyncStorageTest'),
28+
require('./LayoutEventsTest'),
2829
require('./SimpleSnapshotTest'),
2930
];
3031

IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ - (void)testAsyncStorage
7171
[_runner runTest:_cmd module:@"AsyncStorageTest"];
7272
}
7373

74+
- (void)testLayoutEvents
75+
{
76+
[_runner runTest:_cmd module:@"LayoutEventsTest"];
77+
}
78+
7479
#pragma mark Snapshot Tests
7580

7681
- (void)testSimpleSnapshot
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule LayoutEventsTest
10+
* @flow
11+
*/
12+
'use strict';
13+
14+
var React = require('react-native');
15+
var {
16+
Image,
17+
LayoutAnimation,
18+
NativeModules,
19+
StyleSheet,
20+
Text,
21+
View,
22+
} = React;
23+
var TestModule = NativeModules.TestModule || NativeModules.SnapshotTestManager;
24+
25+
var deepDiffer = require('deepDiffer');
26+
27+
function debug() {
28+
//console.log.apply(null, arguments);
29+
}
30+
31+
type LayoutEvent = {
32+
nativeEvent: {
33+
layout: {
34+
x: number;
35+
y: number;
36+
width: number;
37+
height: number;
38+
};
39+
};
40+
};
41+
42+
var LayoutEventsTest = React.createClass({
43+
getInitialState: function() {
44+
return {
45+
didAnimation: false,
46+
};
47+
},
48+
animateViewLayout: function() {
49+
LayoutAnimation.configureNext(
50+
LayoutAnimation.Presets.spring,
51+
() => {
52+
debug('layout animation done.');
53+
this.checkLayout(this.addWrapText);
54+
},
55+
(error) => { throw new Error(JSON.stringify(error)); }
56+
);
57+
this.setState({viewStyle: {margin: 60}});
58+
},
59+
addWrapText: function() {
60+
this.setState(
61+
{extraText: ' And a bunch more text to wrap around a few lines.'},
62+
() => this.checkLayout(this.changeContainer)
63+
);
64+
},
65+
changeContainer: function() {
66+
this.setState(
67+
{containerStyle: {width: 280}},
68+
() => this.checkLayout(TestModule.markTestCompleted)
69+
);
70+
},
71+
checkLayout: function(next?: ?Function) {
72+
if (!this.isMounted()) {
73+
return;
74+
}
75+
this.refs.view.measure((x, y, width, height) => {
76+
this.compare('view', {x, y, width, height}, this.state.viewLayout);
77+
if (typeof next === 'function') {
78+
next();
79+
} else if (!this.state.didAnimation) {
80+
// Trigger first state change after onLayout fires
81+
this.animateViewLayout();
82+
this.state.didAnimation = true;
83+
}
84+
});
85+
this.refs.txt.measure((x, y, width, height) => {
86+
this.compare('txt', {x, y, width, height}, this.state.textLayout);
87+
});
88+
this.refs.img.measure((x, y, width, height) => {
89+
this.compare('img', {x, y, width, height}, this.state.imageLayout);
90+
});
91+
},
92+
compare: function(node: string, measured: any, onLayout: any): void {
93+
if (deepDiffer(measured, onLayout)) {
94+
var data = {measured, onLayout};
95+
throw new Error(
96+
node + ' onLayout mismatch with measure ' +
97+
JSON.stringify(data, null, ' ')
98+
);
99+
}
100+
},
101+
onViewLayout: function(e: LayoutEvent) {
102+
debug('received view layout event\n', e.nativeEvent);
103+
this.setState({viewLayout: e.nativeEvent.layout}, this.checkLayout);
104+
},
105+
onTextLayout: function(e: LayoutEvent) {
106+
debug('received text layout event\n', e.nativeEvent);
107+
this.setState({textLayout: e.nativeEvent.layout}, this.checkLayout);
108+
},
109+
onImageLayout: function(e: LayoutEvent) {
110+
debug('received image layout event\n', e.nativeEvent);
111+
this.setState({imageLayout: e.nativeEvent.layout}, this.checkLayout);
112+
},
113+
render: function() {
114+
var viewStyle = [styles.view, this.state.viewStyle];
115+
var textLayout = this.state.textLayout || {width: '?', height: '?'};
116+
var imageLayout = this.state.imageLayout || {x: '?', y: '?'};
117+
return (
118+
<View style={[styles.container, this.state.containerStyle]}>
119+
<View ref="view" onLayout={this.onViewLayout} style={viewStyle}>
120+
<Image
121+
ref="img"
122+
onLayout={this.onImageLayout}
123+
style={styles.image}
124+
source={{uri: 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png'}}
125+
/>
126+
<Text>
127+
ViewLayout: {JSON.stringify(this.state.viewLayout, null, ' ') + '\n\n'}
128+
</Text>
129+
<Text ref="txt" onLayout={this.onTextLayout} style={styles.text}>
130+
A simple piece of text.{this.state.extraText}
131+
</Text>
132+
<Text>
133+
{'\n'}
134+
Text w/h: {textLayout.width}/{textLayout.height + '\n'}
135+
Image x/y: {imageLayout.x}/{imageLayout.y}
136+
</Text>
137+
</View>
138+
</View>
139+
);
140+
}
141+
});
142+
143+
var styles = StyleSheet.create({
144+
container: {
145+
margin: 40,
146+
},
147+
view: {
148+
margin: 20,
149+
padding: 12,
150+
borderColor: 'black',
151+
borderWidth: 0.5,
152+
backgroundColor: 'transparent',
153+
},
154+
text: {
155+
alignSelf: 'flex-start',
156+
borderColor: 'rgba(0, 0, 255, 0.2)',
157+
borderWidth: 0.5,
158+
},
159+
image: {
160+
width: 50,
161+
height: 50,
162+
marginBottom: 10,
163+
alignSelf: 'center',
164+
},
165+
});
166+
167+
module.exports = LayoutEventsTest;

Libraries/Components/View/View.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ var View = React.createClass({
9090
onStartShouldSetResponder: PropTypes.func,
9191
onStartShouldSetResponderCapture: PropTypes.func,
9292

93+
/**
94+
* Invoked on mount and layout changes with {x, y, width, height}.
95+
*/
96+
onLayout: PropTypes.func,
97+
9398
/**
9499
* In the absence of `auto` property, `none` is much like `CSS`'s `none`
95100
* value. `box-none` is as if you had applied the `CSS` class:

Libraries/ReactIOS/ReactIOSViewAttributes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
* @providesModule ReactIOSViewAttributes
1010
* @flow
1111
*/
12-
13-
"use strict";
12+
'use strict';
1413

1514
var merge = require('merge');
1615

@@ -21,6 +20,7 @@ ReactIOSViewAttributes.UIView = {
2120
accessible: true,
2221
accessibilityLabel: true,
2322
testID: true,
23+
onLayout: true,
2424
};
2525

2626
ReactIOSViewAttributes.RCTView = merge(
@@ -31,7 +31,7 @@ ReactIOSViewAttributes.RCTView = merge(
3131
// For this property to be effective, it must be applied to a view that contains
3232
// many subviews that extend outside its bound. The subviews must also have
3333
// overflow: hidden, as should the containing view (or one of its superviews).
34-
removeClippedSubviews: true
34+
removeClippedSubviews: true,
3535
});
3636

3737
module.exports = ReactIOSViewAttributes;

0 commit comments

Comments
 (0)