mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-24 12:25:59 +08:00
[ReactNative] Element Inspector
Summary:
This adds new development feature to React Native that provides information
about selected element (see the demo in Test Plan).
This is how it works:
App's root component is rendered to a container that also has a hidden layer called
`<InspectorOverlay/>`. When activated, it shows full screen view and captures all
touches. On every touch we ask UIManager to find a view for given {x,y} coordinates.
Then, we use React's internals to find corresponding React component. `setRootInstance`
is used to remember the top level component to start search from, lmk if you have a
better idea how to do this. Given a component, we can climb up its owners tree
to provice more context on how/where the component is used.
In future we could use the `hierarchy` array to inspect and print their props/state.
Known bugs and limitations:
* InspectorOverlay sometimes receives touches with incorrect coordinates (wtf)
* Not integrated with React Chrome Devtools (maybe in followup diffs)
* Doesn't work with popovers (maybe put the element inspector into an `<Overlay/>`?)
@public
Test Plan:
https://www.facebook.com/pxlcld/mn5k
Works nicely with scrollviews
This commit is contained in:
62
Libraries/Inspector.js
Normal file
62
Libraries/Inspector.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule Inspector
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
var ReactInstanceMap = require('ReactInstanceMap');
|
||||
var ReactNativeMount = require('ReactNativeMount');
|
||||
var ReactNativeTagHandles = require('ReactNativeTagHandles');
|
||||
|
||||
function traverseOwnerTreeUp(hierarchy, instance) {
|
||||
if (instance) {
|
||||
hierarchy.unshift(instance);
|
||||
traverseOwnerTreeUp(hierarchy, instance._currentElement._owner);
|
||||
}
|
||||
}
|
||||
|
||||
function findInstance(component, targetID) {
|
||||
if (targetID === findRootNodeID(component)) {
|
||||
return component;
|
||||
}
|
||||
if (component._renderedComponent) {
|
||||
return findInstance(component._renderedComponent, targetID);
|
||||
} else {
|
||||
for (var key in component._renderedChildren) {
|
||||
var child = component._renderedChildren[key];
|
||||
if (ReactInstanceHandles.isAncestorIDOf(findRootNodeID(child), targetID)) {
|
||||
var instance = findInstance(child, targetID);
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findRootNodeID(component) {
|
||||
var internalInstance = ReactInstanceMap.get(component);
|
||||
return internalInstance ? internalInstance._rootNodeID : component._rootNodeID;
|
||||
}
|
||||
|
||||
function findInstanceByNativeTag(rootTag, nativeTag) {
|
||||
var containerID = ReactNativeTagHandles.tagToRootNodeID[rootTag];
|
||||
var rootInstance = ReactNativeMount._instancesByContainerID[containerID];
|
||||
var targetID = ReactNativeTagHandles.tagToRootNodeID[nativeTag];
|
||||
return findInstance(rootInstance, targetID);
|
||||
}
|
||||
|
||||
function getOwnerHierarchy(instance) {
|
||||
var hierarchy = [];
|
||||
traverseOwnerTreeUp(hierarchy, instance);
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
module.exports = {findInstanceByNativeTag, getOwnerHierarchy};
|
||||
112
Libraries/ReactIOS/InspectorOverlay.js
Normal file
112
Libraries/ReactIOS/InspectorOverlay.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule InspectorOverlay
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var Dimensions = require('Dimensions');
|
||||
var Inspector = require('Inspector');
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var Text = require('Text');
|
||||
var UIManager = require('NativeModules').UIManager;
|
||||
var View = require('View');
|
||||
|
||||
var InspectorOverlay = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
frame: null,
|
||||
hierarchy: [],
|
||||
};
|
||||
},
|
||||
|
||||
findViewForTouchEvent: function(e) {
|
||||
var {locationX, locationY} = e.nativeEvent.touches[0];
|
||||
UIManager.findSubviewIn(
|
||||
this.props.inspectedViewTag,
|
||||
[locationX, locationY],
|
||||
(nativeViewTag, left, top, width, height) => {
|
||||
var instance = Inspector.findInstanceByNativeTag(this.props.rootTag, nativeViewTag);
|
||||
var hierarchy = Inspector.getOwnerHierarchy(instance);
|
||||
this.setState({
|
||||
hierarchy,
|
||||
frame: {left, top, width, height}
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
shouldSetResponser: function(e) {
|
||||
this.findViewForTouchEvent(e);
|
||||
return true;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var content = [];
|
||||
|
||||
if (this.state.frame) {
|
||||
var distanceToTop = this.state.frame.top;
|
||||
var distanceToBottom = Dimensions.get('window').height -
|
||||
(this.state.frame.top + this.state.frame.height);
|
||||
|
||||
var justifyContent = distanceToTop > distanceToBottom
|
||||
? 'flex-start'
|
||||
: 'flex-end';
|
||||
|
||||
content.push(<View style={[styles.frame, this.state.frame]} />);
|
||||
content.push(<ElementProperties hierarchy={this.state.hierarchy} />);
|
||||
}
|
||||
return (
|
||||
<View
|
||||
onStartShouldSetResponder={this.shouldSetResponser}
|
||||
onResponderMove={this.findViewForTouchEvent}
|
||||
style={[styles.inspector, {justifyContent}]}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ElementProperties = React.createClass({
|
||||
render: function() {
|
||||
var path = this.props.hierarchy.map((instance) => instance.getName()).join(' > ');
|
||||
return (
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.path}>
|
||||
{path}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
inspector: {
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
frame: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(155,155,255,0.3)',
|
||||
},
|
||||
info: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
padding: 10,
|
||||
},
|
||||
path: {
|
||||
color: 'white',
|
||||
fontSize: 9,
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = InspectorOverlay;
|
||||
@@ -7,17 +7,59 @@
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule renderApplication
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var InspectorOverlay = require('InspectorOverlay');
|
||||
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var Subscribable = require('Subscribable');
|
||||
var View = require('View');
|
||||
var WarningBox = require('WarningBox');
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var AppContainer = React.createClass({
|
||||
mixins: [Subscribable.Mixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return { inspector: null };
|
||||
},
|
||||
|
||||
toggleElementInspector: function() {
|
||||
var inspector = this.state.inspector
|
||||
? null
|
||||
: <InspectorOverlay
|
||||
rootTag={this.props.rootTag}
|
||||
inspectedViewTag={React.findNodeHandle(this.refs.main)}
|
||||
/>;
|
||||
this.setState({inspector});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.addListenerOn(
|
||||
RCTDeviceEventEmitter,
|
||||
'toggleElementInspector',
|
||||
this.toggleElementInspector
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled;
|
||||
var warningBox = shouldRenderWarningBox ? <WarningBox /> : null;
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
<View style={styles.appContainer} ref="main">
|
||||
{this.props.children}
|
||||
</View>
|
||||
{warningBox}
|
||||
{this.state.inspector}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function renderApplication<D, P, S>(
|
||||
RootComponent: ReactClass<D, P, S>,
|
||||
initialProps: P,
|
||||
@@ -27,15 +69,12 @@ function renderApplication<D, P, S>(
|
||||
rootTag,
|
||||
'Expect to have a valid rootTag, instead got ', rootTag
|
||||
);
|
||||
var shouldRenderWarningBox = __DEV__ && console.yellowBoxEnabled;
|
||||
var warningBox = shouldRenderWarningBox ? <WarningBox /> : null;
|
||||
React.render(
|
||||
<View style={styles.appContainer}>
|
||||
<AppContainer rootTag={rootTag}>
|
||||
<RootComponent
|
||||
{...initialProps}
|
||||
/>
|
||||
{warningBox}
|
||||
</View>,
|
||||
</AppContainer>,
|
||||
rootTag
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#import "RCTBridge.h"
|
||||
#import "RCTDefines.h"
|
||||
#import "RCTEventDispatcher.h"
|
||||
#import "RCTKeyCommands.h"
|
||||
#import "RCTLog.h"
|
||||
#import "RCTPerfStats.h"
|
||||
@@ -241,6 +242,8 @@ RCT_EXPORT_METHOD(show)
|
||||
destructiveButtonTitle:nil
|
||||
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil];
|
||||
|
||||
[actionSheet addButtonWithTitle:@"Inspect Element"];
|
||||
|
||||
if (_liveReloadURL) {
|
||||
|
||||
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
|
||||
@@ -300,10 +303,14 @@ RCT_EXPORT_METHOD(reload)
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
self.liveReloadEnabled = !_liveReloadEnabled;
|
||||
[_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
self.liveReloadEnabled = !_liveReloadEnabled;
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
self.profilingEnabled = !_profilingEnabled;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -882,6 +882,31 @@ RCT_EXPORT_METHOD(blur:(NSNumber *)reactTag)
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(findSubviewIn:(NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback) {
|
||||
if (!reactTag) {
|
||||
callback(@[[NSNull null]]);
|
||||
return;
|
||||
}
|
||||
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
UIView *view = viewRegistry[reactTag];
|
||||
UIView *target = [view hitTest:point withEvent:nil];
|
||||
CGRect frame = [target convertRect:target.bounds toView:view];
|
||||
|
||||
while (target.reactTag == nil && target.superview != nil) {
|
||||
target = [target superview];
|
||||
}
|
||||
|
||||
callback(@[
|
||||
target.reactTag ?: [NSNull null],
|
||||
@(frame.origin.x),
|
||||
@(frame.origin.y),
|
||||
@(frame.size.width),
|
||||
@(frame.size.height),
|
||||
]);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)batchDidComplete
|
||||
{
|
||||
// Gather blocks to be executed now that all view hierarchy manipulations have
|
||||
|
||||
Reference in New Issue
Block a user