Add a nativeID prop to allow native code to reference react managed views

Reviewed By: sahrens

Differential Revision: D4786713

fbshipit-source-id: af9cef0737c010b429d52d00181c00bd81f13f5b
This commit is contained in:
Andrew Y. Chen 2017-04-07 11:47:35 -07:00 committed by Facebook Github Bot
parent e154117f37
commit 909af08f24
17 changed files with 218 additions and 4 deletions

View File

@ -698,6 +698,7 @@ const TextInput = React.createClass({
accessible={props.accessible}
accessibilityLabel={props.accessibilityLabel}
accessibilityTraits={props.accessibilityTraits}
nativeID={this.props.nativeID}
testID={props.testID}>
{textContainer}
</TouchableWithoutFeedback>
@ -748,6 +749,7 @@ const TextInput = React.createClass({
accessible={this.props.accessible}
accessibilityLabel={this.props.accessibilityLabel}
accessibilityComponentType={this.props.accessibilityComponentType}
nativeID={this.props.nativeID}
testID={this.props.testID}>
{textContainer}
</TouchableWithoutFeedback>

View File

@ -141,6 +141,7 @@ var TouchableBounce = React.createClass({
accessibilityLabel={this.props.accessibilityLabel}
accessibilityComponentType={this.props.accessibilityComponentType}
accessibilityTraits={this.props.accessibilityTraits}
nativeID={this.props.nativeID}
testID={this.props.testID}
hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}

View File

@ -22,7 +22,6 @@ var TimerMixin = require('react-timer-mixin');
var Touchable = require('Touchable');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
var View = require('View');
const ViewPropTypes = require('ViewPropTypes');
var ensureComponentIsNative = require('ensureComponentIsNative');
@ -267,6 +266,7 @@ var TouchableHighlight = React.createClass({
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}
nativeID={this.props.nativeID}
testID={this.props.testID}>
{React.cloneElement(
React.Children.only(this.props.children),

View File

@ -165,7 +165,7 @@ var TouchableOpacity = React.createClass({
_opacityFocused: function() {
this.setOpacityTo(this.props.focusedOpacity);
},
_getChildStyleOpacityWithDefault: function() {
var childStyle = flattenStyle(this.props.style) || {};
return childStyle.opacity == undefined ? 1 : childStyle.opacity;
@ -179,6 +179,7 @@ var TouchableOpacity = React.createClass({
accessibilityComponentType={this.props.accessibilityComponentType}
accessibilityTraits={this.props.accessibilityTraits}
style={[this.props.style, {opacity: this.state.anim}]}
nativeID={this.props.nativeID}
testID={this.props.testID}
onLayout={this.props.onLayout}
isTVSelectable={true}

View File

@ -176,6 +176,8 @@ const TouchableWithoutFeedback = React.createClass({
accessibilityComponentType: this.props.accessibilityComponentType,
accessibilityTraits: this.props.accessibilityTraits,
// $FlowFixMe(>=0.41.0)
nativeID: this.props.nativeID,
// $FlowFixMe(>=0.41.0)
testID: this.props.testID,
onLayout: this.props.onLayout,
hitSlop: this.props.hitSlop,

View File

@ -9,6 +9,7 @@ exports[`TouchableHighlight renders correctly 1`] = `
hasTVPreferredFocus={undefined}
hitSlop={undefined}
isTVSelectable={true}
nativeID={undefined}
onLayout={undefined}
onResponderGrant={[Function]}
onResponderMove={[Function]}

View File

@ -23,6 +23,7 @@ ReactNativeViewAttributes.UIView = {
accessibilityLiveRegion: true,
accessibilityTraits: true,
importantForAccessibility: true,
nativeID: true,
testID: true,
renderToHardwareTextureAndroid: true,
shouldRasterizeIOS: true,

View File

@ -173,6 +173,15 @@ module.exports = {
*/
testID: PropTypes.string,
/**
* Used to locate this view from native classes.
*
* > This disables the 'layout-only view removal' optimization for this view!
*
* @platform android
*/
nativeID: PropTypes.string,
/**
* For most touch interactions, you'll simply want to wrap your component in
* `TouchableHighlight` or `TouchableOpacity`. Check out `Touchable.js`,

View File

@ -21,9 +21,9 @@ const StyleSheetPropType = require('StyleSheetPropType');
const TextStylePropTypes = require('TextStylePropTypes');
const Touchable = require('Touchable');
const processColor = require('processColor');
const createReactNativeComponentClass = require('createReactNativeComponentClass');
const mergeFast = require('mergeFast');
const processColor = require('processColor');
const { PropTypes } = React;
@ -177,6 +177,11 @@ const Text = React.createClass({
* Used to locate this view in end-to-end tests.
*/
testID: PropTypes.string,
/**
* Used to locate this view from native code.
* @platform android
*/
nativeID: PropTypes.string,
/**
* Specifies whether fonts should scale to respect Text Size accessibility settings. The
* default is `true`.

View File

@ -13,13 +13,14 @@ deps = [
react_native_target("java/com/facebook/react/modules/appstate:appstate"),
react_native_target("java/com/facebook/react/modules/core:core"),
react_native_target("java/com/facebook/react/modules/datepicker:datepicker"),
react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"),
react_native_target("java/com/facebook/react/modules/share:share"),
react_native_target("java/com/facebook/react/modules/systeminfo:systeminfo"),
react_native_target("java/com/facebook/react/modules/deviceinfo:deviceinfo"),
react_native_target("java/com/facebook/react/modules/timepicker:timepicker"),
react_native_target("java/com/facebook/react/touch:touch"),
react_native_target("java/com/facebook/react/uimanager:uimanager"),
react_native_target("java/com/facebook/react/uimanager/annotations:annotations"),
react_native_target("java/com/facebook/react/uimanager/util:util"),
react_native_target("java/com/facebook/react/views/picker:picker"),
react_native_target("java/com/facebook/react/views/progressbar:progressbar"),
react_native_target("java/com/facebook/react/views/scroll:scroll"),

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2014-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.
*/
package com.facebook.react.tests;
import java.util.Arrays;
import java.util.List;
import android.view.View;
import com.facebook.react.testing.ReactAppInstrumentationTestCase;
import com.facebook.react.uimanager.util.ReactFindViewUtil;
/**
* Tests that the 'nativeID' property can be set on various views.
* The 'nativeID' property is used to reference react managed views from native code.
*/
public class NativeIdTestCase extends ReactAppInstrumentationTestCase {
@Override
protected String getReactApplicationKeyUnderTest() {
return "NativeIdTestApp";
}
private final List<String> viewTags = Arrays.asList(
"Image",
"Text",
"TouchableBounce",
"TouchableHighlight",
"TouchableOpacity",
"TouchableWithoutFeedback",
"TextInput",
"View");
public void testPropertyIsSetForViews() {
for (String nativeId : viewTags) {
View viewWithTag = ReactFindViewUtil.findViewByNativeId(
getActivity().getRootView(),
nativeId);
assertNotNull(
"View with nativeID " + nativeId + " was not found. Check NativeIdTestModule.js.",
viewWithTag);
}
}
}

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) 2013-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 NativeIdTestModule
* @flow
*/
'use strict';
const Image = require('Image');
const React = require('React');
const StyleSheet = require('StyleSheet');
const Text = require('Text');
const TextInput = require('TextInput');
const TouchableBounce = require('TouchableBounce');
const TouchableHighlight = require('TouchableHighlight');
const TouchableOpacity = require('TouchableOpacity');
const TouchableWithoutFeedback = require('TouchableWithoutFeedback');
const View = require('View');
/**
* All the views implemented on Android, each with the nativeID property set.
* We test that:
* - The app renders fine
* - The nativeID property is passed to the native views
*/
class NativeIdTestApp extends React.Component {
render() {
const uri = 'data:image/gif;base64,' +
'R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAwAAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapy' +
'uvUUlvONmOZtfzgFzByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSpa/' +
'TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJlZeGl9i2icVqaNVailT6F5' +
'iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uisF81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97V' +
'riy/Xl4/f1cf5VWzXyym7PHhhx4dbgYKAAA7';
return (
<View>
<Image
nativeID="Image"
source={{uri: uri}}
style={styles.base} />
<Text nativeID="Text">text</Text>
<TextInput nativeID="TextInput" value="Text input" />
<TouchableBounce nativeID="TouchableBounce">
<Text>TouchableBounce</Text>
</TouchableBounce>
<TouchableHighlight nativeID="TouchableHighlight">
<Text>TouchableHighlight</Text>
</TouchableHighlight>
<TouchableOpacity nativeID="TouchableOpacity">
<Text>TouchableOpacity</Text>
</TouchableOpacity>
<TouchableWithoutFeedback nativeID="TouchableWithoutFeedback">
<View>
<Text>TouchableWithoutFeedback</Text>
</View>
</TouchableWithoutFeedback>
<View nativeID="View" />
</View>
);
}
}
const styles = StyleSheet.create({
base: {
width: 150,
height: 50,
},
});
module.exports = {
NativeIdTestApp: NativeIdTestApp,
};

View File

@ -69,6 +69,10 @@ var apps = [
appKey: 'MultitouchHandlingTestAppModule',
component: () => require('MultitouchHandlingTestAppModule')
},
{
appKey: 'NativeIdTestApp',
component: () => require('NativeIdTestModule').NativeIdTestApp
},
{
appKey: 'PickerAndroidTestApp',
component: () => require('PickerAndroidTestModule').PickerAndroidTestApp,

View File

@ -41,6 +41,7 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
* Used to locate views in end-to-end (UI) tests.
*/
public static final String PROP_TEST_ID = "testID";
public static final String PROP_NATIVE_ID = "nativeID";
private static MatrixMathHelper.MatrixDecompositionContext sMatrixDecompositionContext =
new MatrixMathHelper.MatrixDecompositionContext();
@ -92,6 +93,11 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
view.setTag(testId);
}
@ReactProp(name = PROP_NATIVE_ID)
public void setNativeId(T view, String nativeId) {
view.setTag(R.id.view_tag_native_id, nativeId);
}
@ReactProp(name = PROP_ACCESSIBILITY_LABEL)
public void setAccessibilityLabel(T view, String accessibilityLabel) {
view.setContentDescription(accessibilityLabel);

View File

@ -0,0 +1,13 @@
include_defs("//ReactAndroid/DEFS")
android_library(
name = "util",
srcs = glob(["*.java"]),
visibility = [
"PUBLIC",
],
deps = [
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("res:uimanager"),
],
)

View File

@ -0,0 +1,38 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.uimanager.util;
import javax.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.R;
/**
* Finds views in React Native view hierarchies
*/
public class ReactFindViewUtil {
/**
* Finds a view that is tagged with {@param nativeId} as its `nativeID` prop
*/
public static @Nullable View findViewByNativeId(View view, String nativeId) {
Object tag = view.getTag(R.id.view_tag_native_id);
if (tag instanceof String && tag.equals(nativeId)) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View v = findViewByNativeId(viewGroup.getChildAt(i), nativeId);
if (v != null) {
return v;
}
}
}
return null;
}
}

View File

@ -2,4 +2,7 @@
<resources>
<!-- tag is used to store the testID tag -->
<item type="id" name="react_test_id"/>
<!-- tag is used to store the nativeID tag -->
<item type="id" name="view_tag_native_id"/>
</resources>