use DynamicFromObject to avoid maps

Summary:
Changes our property access pattern to iterate through props once and pass the Object value directly rather than looking the value up in the map with the key.

Note some ViewManagers methods (especially yoga related ones on shadow nodes) expect a `Dyanamic`, so this diff also creates Dynamic's only when needed by the hand-written code, and introduces a new `DynamicWithObject` to create them that simply wraps the underlying object (as opposed to `DynamicWithMap` which wraps the map and does a lookup any time the `Dynamic` is accessed.

Reviewed By: mdvacca

Differential Revision: D14453300

fbshipit-source-id: df98567b6eff1e6b7c611f179eb11e413fb94e5d
This commit is contained in:
Spencer Ahrens 2019-03-25 12:04:36 -07:00 коммит произвёл Facebook Github Bot
Родитель af38a0cf87
Коммит a46fba5dd3
8 изменённых файлов: 422 добавлений и 246 удалений

Просмотреть файл

@ -0,0 +1,88 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.bridge;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import javax.annotation.Nullable;
/**
* Implementation of Dynamic wrapping a ReadableArray.
*/
public class DynamicFromObject implements Dynamic {
private @Nullable Object mObject;
public DynamicFromObject(@Nullable Object obj) {
mObject = obj;
}
@Override
public void recycle() {
// Noop - nothing to recycle since there is no pooling
}
@Override
public boolean isNull() {
return mObject == null;
}
@Override
public boolean asBoolean() {
return (boolean)mObject;
}
@Override
public double asDouble() {
return (double)mObject;
}
@Override
public int asInt() {
// Numbers from JS are always Doubles
return ((Double)mObject).intValue();
}
@Override
public String asString() {
return (String)mObject;
}
@Override
public ReadableArray asArray() {
return (ReadableArray)mObject;
}
@Override
public ReadableMap asMap() {
return (ReadableMap)mObject;
}
@Override
public ReadableType getType() {
if (isNull()) {
return ReadableType.Null;
}
if (mObject instanceof Boolean) {
return ReadableType.Boolean;
}
if (mObject instanceof Number) {
return ReadableType.Number;
}
if (mObject instanceof String) {
return ReadableType.String;
}
if (mObject instanceof ReadableMap) {
return ReadableType.Map;
}
if (mObject instanceof ReadableArray) {
return ReadableType.Array;
}
FLog.e(ReactConstants.TAG, "Unmapped object type " + mObject.getClass().getName());
return ReadableType.Null;
}
}

Просмотреть файл

@ -1,10 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.react.bridge;
import java.util.HashMap;
@ -20,9 +19,9 @@ import javax.annotation.Nullable;
* of {@link WritableNativeMap} created via {@link Arguments#createMap} or just {@link ReadableMap}
* interface if you want your "native" module method to take a map from JS as an argument.
*
* Main purpose for this class is to be used in java-only unit tests, but could also be used outside
* of tests in the code that operates only in java and needs to communicate with RN modules via
* their JS-exposed API.
* <p>Main purpose for this class is to be used in java-only unit tests, but could also be used
* outside of tests in the code that operates only in java and needs to communicate with RN modules
* via their JS-exposed API.
*/
public class JavaOnlyMap implements ReadableMap, WritableMap {
@ -62,16 +61,19 @@ public class JavaOnlyMap implements ReadableMap, WritableMap {
return res;
}
/**
* @param keysAndValues keys and values, interleaved
*/
/** @param keysAndValues keys and values, interleaved */
private JavaOnlyMap(Object... keysAndValues) {
if (keysAndValues.length % 2 != 0) {
throw new IllegalArgumentException("You must provide the same number of keys and values");
}
mBackingMap = new HashMap();
for (int i = 0; i < keysAndValues.length; i += 2) {
mBackingMap.put(keysAndValues[i], keysAndValues[i + 1]);
Object val = keysAndValues[i + 1];
if (val instanceof Number) {
// all values from JS are doubles, so emulate that here for tests.
val = ((Number)val).doubleValue();
}
mBackingMap.put(keysAndValues[i], val);
}
}
@ -142,15 +144,20 @@ public class JavaOnlyMap implements ReadableMap, WritableMap {
} else if (value instanceof Dynamic) {
return ((Dynamic) value).getType();
} else {
throw new IllegalArgumentException("Invalid value " + value.toString() + " for key " + name +
"contained in JavaOnlyMap");
throw new IllegalArgumentException(
"Invalid value " + value.toString() + " for key " + name + "contained in JavaOnlyMap");
}
}
@Override
public @Nonnull Iterator<Map.Entry<String, Object>> getEntryIterator() {
return mBackingMap.entrySet().iterator();
}
@Override
public @Nonnull ReadableMapKeySetIterator keySetIterator() {
return new ReadableMapKeySetIterator() {
Iterator<String> mIterator = mBackingMap.keySet().iterator();
Iterator<Map.Entry<String, Object>> mIterator = mBackingMap.entrySet().iterator();
@Override
public boolean hasNextKey() {
@ -159,7 +166,7 @@ public class JavaOnlyMap implements ReadableMap, WritableMap {
@Override
public String nextKey() {
return mIterator.next();
return mIterator.next().getKey();
}
};
}

Просмотреть файл

@ -1,13 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.react.bridge;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -19,16 +20,36 @@ import javax.annotation.Nullable;
public interface ReadableMap {
boolean hasKey(@Nonnull String name);
boolean isNull(@Nonnull String name);
boolean getBoolean(@Nonnull String name);
double getDouble(@Nonnull String name);
int getInt(@Nonnull String name);
@Nullable String getString(@Nonnull String name);
@Nullable ReadableArray getArray(@Nonnull String name);
@Nullable ReadableMap getMap(@Nonnull String name);
@Nonnull Dynamic getDynamic(@Nonnull String name);
@Nonnull ReadableType getType(@Nonnull String name);
@Nonnull ReadableMapKeySetIterator keySetIterator();
@Nonnull HashMap<String, Object> toHashMap();
boolean isNull(@Nonnull String name);
boolean getBoolean(@Nonnull String name);
double getDouble(@Nonnull String name);
int getInt(@Nonnull String name);
@Nullable
String getString(@Nonnull String name);
@Nullable
ReadableArray getArray(@Nonnull String name);
@Nullable
ReadableMap getMap(@Nonnull String name);
@Nonnull
Dynamic getDynamic(@Nonnull String name);
@Nonnull
ReadableType getType(@Nonnull String name);
@Nonnull
Iterator<Map.Entry<String, Object>> getEntryIterator();
@Nonnull
ReadableMapKeySetIterator keySetIterator();
@Nonnull
HashMap<String, Object> toHashMap();
}

Просмотреть файл

@ -1,10 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.facebook.react.bridge;
import com.facebook.infer.annotation.Assertions;
@ -13,6 +12,7 @@ import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.config.ReactFeatureFlags;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -32,17 +32,19 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
private @Nullable String[] mKeys;
private @Nullable HashMap<String,Object> mLocalMap;
private @Nullable HashMap<String,ReadableType> mLocalTypeMap;
private @Nullable HashMap<String, Object> mLocalMap;
private @Nullable HashMap<String, ReadableType> mLocalTypeMap;
private static int mJniCallCounter;
public static void setUseNativeAccessor(boolean useNativeAccessor) {
ReactFeatureFlags.useMapNativeAccessor = useNativeAccessor;
}
public static int getJNIPassCounter() {
return mJniCallCounter;
}
private HashMap<String,Object> getLocalMap() {
private HashMap<String, Object> getLocalMap() {
// Fast return for the common case
if (mLocalMap != null) {
return mLocalMap;
@ -58,17 +60,19 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
mJniCallCounter++;
int length = mKeys.length;
mLocalMap = new HashMap<>(length);
for(int i = 0; i< length; i++) {
for (int i = 0; i < length; i++) {
mLocalMap.put(mKeys[i], values[i]);
}
}
}
return mLocalMap;
}
private native String[] importKeys();
private native Object[] importValues();
private @Nonnull HashMap<String,ReadableType> getLocalTypeMap() {
private @Nonnull HashMap<String, ReadableType> getLocalTypeMap() {
// Fast and non-blocking return for common case
if (mLocalTypeMap != null) {
return mLocalTypeMap;
@ -85,13 +89,14 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
mJniCallCounter++;
int length = mKeys.length;
mLocalTypeMap = new HashMap<>(length);
for(int i = 0; i< length; i++) {
for (int i = 0; i < length; i++) {
mLocalTypeMap.put(mKeys[i], (ReadableType) types[i]);
}
}
}
return mLocalTypeMap;
}
private native Object[] importTypes();
@Override
@ -102,6 +107,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getLocalMap().containsKey(name);
}
private native boolean hasKeyNative(String name);
@Override
@ -115,6 +121,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
throw new NoSuchKeyException(name);
}
private native boolean isNullNative(@Nonnull String name);
private @Nonnull Object getValue(@Nonnull String name) {
@ -146,8 +153,12 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
private void checkInstance(String name, Object value, Class type) {
if (value != null && !type.isInstance(value)) {
throw new ClassCastException(
"Value for " + name + " cannot be cast from " +
value.getClass().getSimpleName() + " to " + type.getSimpleName());
"Value for "
+ name
+ " cannot be cast from "
+ value.getClass().getSimpleName()
+ " to "
+ type.getSimpleName());
}
}
@ -159,6 +170,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getValue(name, Boolean.class).booleanValue();
}
private native boolean getBooleanNative(String name);
@Override
@ -169,6 +181,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getValue(name, Double.class).doubleValue();
}
private native double getDoubleNative(String name);
@Override
@ -181,6 +194,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
// All numbers coming out of native are doubles, so cast here then truncate
return getValue(name, Double.class).intValue();
}
private native int getIntNative(String name);
@Override
@ -191,6 +205,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getNullableValue(name, String.class);
}
private native String getStringNative(String name);
@Override
@ -201,6 +216,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getNullableValue(name, ReadableArray.class);
}
private native ReadableNativeArray getArrayNative(String name);
@Override
@ -211,6 +227,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
return getNullableValue(name, ReadableNativeMap.class);
}
private native ReadableNativeMap getMapNative(String name);
@Override
@ -224,6 +241,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
}
throw new NoSuchKeyException(name);
}
private native ReadableType getTypeNative(String name);
@Override
@ -231,6 +249,11 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
return DynamicFromMap.create(this, name);
}
@Override
public @Nonnull Iterator<Map.Entry<String, Object>> getEntryIterator() {
return getLocalMap().entrySet().iterator();
}
@Override
public @Nonnull ReadableMapKeySetIterator keySetIterator() {
return new ReadableNativeMapKeySetIterator(this);
@ -283,8 +306,8 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
break;
default:
throw new IllegalArgumentException("Could not convert object with key: " + key + ".");
}
}
}
return hashMap;
}
@ -314,17 +337,13 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
return hashMap;
}
/**
* Implementation of a {@link ReadableNativeMap} iterator in native memory.
*/
/** Implementation of a {@link ReadableNativeMap} iterator in native memory. */
@DoNotStrip
private static class ReadableNativeMapKeySetIterator implements ReadableMapKeySetIterator {
@DoNotStrip
private final HybridData mHybridData;
@DoNotStrip private final HybridData mHybridData;
// Need to hold a strong ref to the map so that our native references remain valid.
@DoNotStrip
private final ReadableNativeMap mMap;
@DoNotStrip private final ReadableNativeMap mMap;
public ReadableNativeMapKeySetIterator(ReadableNativeMap readableNativeMap) {
mMap = readableNativeMap;
@ -333,6 +352,7 @@ public class ReadableNativeMap extends NativeMap implements ReadableMap {
@Override
public native boolean hasNextKey();
@Override
public native String nextKey();

Просмотреть файл

@ -13,6 +13,7 @@ import static javax.tools.Diagnostic.Kind.WARNING;
import com.facebook.infer.annotation.SuppressFieldNotInitialized;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.DynamicFromObject;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
@ -55,9 +56,9 @@ import javax.lang.model.util.Types;
/**
* This annotation processor crawls subclasses of ReactShadowNode and ViewManager and finds their
* exported properties with the @ReactProp or @ReactGroupProp annotation. It generates a class
* per shadow node/view manager that is named {@code <classname>$$PropSetter}. This class contains methods
* to retrieve the name and type of all methods and a way to set these properties without
* exported properties with the @ReactProp or @ReactGroupProp annotation. It generates a class per
* shadow node/view manager that is named {@code <classname>$$PropSetter}. This class contains
* methods to retrieve the name and type of all methods and a way to set these properties without
* reflection.
*/
@SupportedAnnotationTypes("com.facebook.react.uimanager.annotations.ReactPropertyHolder")
@ -68,10 +69,12 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private static final TypeName PROPS_TYPE =
ClassName.get("com.facebook.react.uimanager", "ReactStylesDiffMap");
private static final TypeName OBJECT_TYPE = TypeName.get(Object.class);
private static final TypeName STRING_TYPE = TypeName.get(String.class);
private static final TypeName READABLE_MAP_TYPE = TypeName.get(ReadableMap.class);
private static final TypeName READABLE_ARRAY_TYPE = TypeName.get(ReadableArray.class);
private static final TypeName DYNAMIC_TYPE = TypeName.get(Dynamic.class);
private static final TypeName DYNAMIC_FROM_OBJECT_TYPE = TypeName.get(DynamicFromObject.class);
private static final TypeName VIEW_MANAGER_TYPE =
ClassName.get("com.facebook.react.uimanager", "ViewManager");
@ -80,14 +83,10 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private static final ClassName VIEW_MANAGER_SETTER_TYPE =
ClassName.get(
"com.facebook.react.uimanager",
"ViewManagerPropertyUpdater",
"ViewManagerSetter");
"com.facebook.react.uimanager", "ViewManagerPropertyUpdater", "ViewManagerSetter");
private static final ClassName SHADOW_NODE_SETTER_TYPE =
ClassName.get(
"com.facebook.react.uimanager",
"ViewManagerPropertyUpdater",
"ShadowNodeSetter");
"com.facebook.react.uimanager", "ViewManagerPropertyUpdater", "ShadowNodeSetter");
private static final TypeName PROPERTY_MAP_TYPE =
ParameterizedTypeName.get(Map.class, String.class, String.class);
@ -96,14 +95,10 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private final Map<ClassName, ClassInfo> mClasses;
@SuppressFieldNotInitialized
private Filer mFiler;
@SuppressFieldNotInitialized
private Messager mMessager;
@SuppressFieldNotInitialized
private Elements mElements;
@SuppressFieldNotInitialized
private Types mTypes;
@SuppressFieldNotInitialized private Filer mFiler;
@SuppressFieldNotInitialized private Messager mMessager;
@SuppressFieldNotInitialized private Elements mElements;
@SuppressFieldNotInitialized private Types mTypes;
static {
DEFAULT_TYPES = new HashMap<>();
@ -165,7 +160,8 @@ public class ReactPropertyProcessor extends AbstractProcessor {
if (!shouldIgnoreClass(classInfo)) {
// Sort by name
Collections.sort(
classInfo.mProperties, new Comparator<PropertyInfo>() {
classInfo.mProperties,
new Comparator<PropertyInfo>() {
@Override
public int compare(PropertyInfo a, PropertyInfo b) {
return a.mProperty.name().compareTo(b.mProperty.name());
@ -219,8 +215,8 @@ public class ReactPropertyProcessor extends AbstractProcessor {
classInfo.addProperty(propertyBuilder.build(element, new RegularProperty(prop)));
} else if (propGroup != null) {
for (int i = 0, size = propGroup.names().length; i < size; i++) {
classInfo
.addProperty(propertyBuilder.build(element, new GroupProperty(propGroup, i)));
classInfo.addProperty(
propertyBuilder.build(element, new GroupProperty(propGroup, i)));
}
}
} catch (ReactPropertyException e) {
@ -251,29 +247,32 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private void generateCode(ClassInfo classInfo, List<PropertyInfo> properties)
throws IOException, ReactPropertyException {
MethodSpec getMethods = MethodSpec.methodBuilder("getProperties")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.addParameter(PROPERTY_MAP_TYPE, "props")
.returns(TypeName.VOID)
.addCode(generateGetProperties(properties))
.build();
MethodSpec getMethods =
MethodSpec.methodBuilder("getProperties")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.addParameter(PROPERTY_MAP_TYPE, "props")
.returns(TypeName.VOID)
.addCode(generateGetProperties(properties))
.build();
TypeName superType = getSuperType(classInfo);
ClassName className = classInfo.mClassName;
String holderClassName =
getClassName((TypeElement) classInfo.mElement, className.packageName()) + "$$PropsSetter";
TypeSpec holderClass = TypeSpec.classBuilder(holderClassName)
.addSuperinterface(superType)
.addModifiers(PUBLIC)
.addMethod(generateSetPropertySpec(classInfo, properties))
.addMethod(getMethods)
.build();
TypeSpec holderClass =
TypeSpec.classBuilder(holderClassName)
.addSuperinterface(superType)
.addModifiers(PUBLIC)
.addMethod(generateSetPropertySpec(classInfo, properties))
.addMethod(getMethods)
.build();
JavaFile javaFile = JavaFile.builder(className.packageName(), holderClass)
.addFileComment("Generated by " + getClass().getName())
.build();
JavaFile javaFile =
JavaFile.builder(className.packageName(), holderClass)
.addFileComment("Generated by " + getClass().getName())
.build();
javaFile.writeTo(mFiler);
}
@ -287,9 +286,7 @@ public class ReactPropertyProcessor extends AbstractProcessor {
switch (classInfo.getType()) {
case VIEW_MANAGER:
return ParameterizedTypeName.get(
VIEW_MANAGER_SETTER_TYPE,
classInfo.mClassName,
classInfo.mViewType);
VIEW_MANAGER_SETTER_TYPE, classInfo.mClassName, classInfo.mViewType);
case SHADOW_NODE:
return ParameterizedTypeName.get(SHADOW_NODE_SETTER_TYPE, classInfo.mClassName);
default:
@ -298,12 +295,12 @@ public class ReactPropertyProcessor extends AbstractProcessor {
}
private static MethodSpec generateSetPropertySpec(
ClassInfo classInfo,
List<PropertyInfo> properties) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("setProperty")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.VOID);
ClassInfo classInfo, List<PropertyInfo> properties) {
MethodSpec.Builder builder =
MethodSpec.methodBuilder("setProperty")
.addModifiers(PUBLIC)
.addAnnotation(Override.class)
.returns(TypeName.VOID);
switch (classInfo.getType()) {
case VIEW_MANAGER:
@ -312,14 +309,13 @@ public class ReactPropertyProcessor extends AbstractProcessor {
.addParameter(classInfo.mViewType, "view");
break;
case SHADOW_NODE:
builder
.addParameter(classInfo.mClassName, "node");
builder.addParameter(classInfo.mClassName, "node");
break;
}
return builder
.addParameter(STRING_TYPE, "name")
.addParameter(PROPS_TYPE, "props")
.addParameter(OBJECT_TYPE, "value")
.addCode(generateSetProperty(classInfo, properties))
.build();
}
@ -334,9 +330,7 @@ public class ReactPropertyProcessor extends AbstractProcessor {
builder.add("switch (name) {\n").indent();
for (int i = 0, size = properties.size(); i < size; i++) {
PropertyInfo propertyInfo = properties.get(i);
builder
.add("case \"$L\":\n", propertyInfo.mProperty.name())
.indent();
builder.add("case \"$L\":\n", propertyInfo.mProperty.name()).indent();
switch (info.getType()) {
case VIEW_MANAGER:
@ -350,14 +344,12 @@ public class ReactPropertyProcessor extends AbstractProcessor {
builder.add("$L, ", ((GroupProperty) propertyInfo.mProperty).mGroupIndex);
}
if (BOXED_PRIMITIVES.contains(propertyInfo.propertyType)) {
builder.add("props.isNull(name) ? null : ");
builder.add("value == null ? null : ");
}
getPropertyExtractor(propertyInfo, builder);
builder.addStatement(")");
builder
.addStatement("break")
.unindent();
builder.addStatement("break").unindent();
}
builder.unindent().add("}\n");
@ -365,17 +357,16 @@ public class ReactPropertyProcessor extends AbstractProcessor {
}
private static CodeBlock.Builder getPropertyExtractor(
PropertyInfo info,
CodeBlock.Builder builder) {
PropertyInfo info, CodeBlock.Builder builder) {
TypeName propertyType = info.propertyType;
if (propertyType.equals(STRING_TYPE)) {
return builder.add("props.getString(name)");
return builder.add("($L)value", STRING_TYPE);
} else if (propertyType.equals(READABLE_ARRAY_TYPE)) {
return builder.add("props.getArray(name)");
return builder.add("($L)value", READABLE_ARRAY_TYPE); // TODO: use real type but needs import
} else if (propertyType.equals(READABLE_MAP_TYPE)) {
return builder.add("props.getMap(name)");
return builder.add("($L)value", READABLE_MAP_TYPE);
} else if (propertyType.equals(DYNAMIC_TYPE)) {
return builder.add("props.getDynamic(name)");
return builder.add("new $L(value)", DYNAMIC_FROM_OBJECT_TYPE);
}
if (BOXED_PRIMITIVES.contains(propertyType)) {
@ -383,25 +374,27 @@ public class ReactPropertyProcessor extends AbstractProcessor {
}
if (propertyType.equals(TypeName.BOOLEAN)) {
return builder.add("props.getBoolean(name, $L)", info.mProperty.defaultBoolean());
} if (propertyType.equals(TypeName.DOUBLE)) {
return builder.add("value == null ? $L : (boolean) value", info.mProperty.defaultBoolean());
}
if (propertyType.equals(TypeName.DOUBLE)) {
double defaultDouble = info.mProperty.defaultDouble();
if (Double.isNaN(defaultDouble)) {
return builder.add("props.getDouble(name, $T.NaN)", Double.class);
return builder.add("value == null ? $T.NaN : (double) value", Double.class);
} else {
return builder.add("props.getDouble(name, $Lf)", defaultDouble);
return builder.add("value == null ? $Lf : (double) value", defaultDouble);
}
}
if (propertyType.equals(TypeName.FLOAT)) {
float defaultFloat = info.mProperty.defaultFloat();
if (Float.isNaN(defaultFloat)) {
return builder.add("props.getFloat(name, $T.NaN)", Float.class);
return builder.add("value == null ? $T.NaN : ((Double)value).floatValue()", Float.class);
} else {
return builder.add("props.getFloat(name, $Lf)", defaultFloat);
return builder.add("value == null ? $Lf : ((Double)value).floatValue()", defaultFloat);
}
}
if (propertyType.equals(TypeName.INT)) {
return builder.add("props.getInt(name, $L)", info.mProperty.defaultInt());
return builder.add(
"value == null ? $L : ((Double)value).intValue()", info.mProperty.defaultInt());
}
throw new IllegalArgumentException();
@ -424,20 +417,20 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private static String getPropertypTypeName(Property property, TypeName propertyType) {
String defaultType = DEFAULT_TYPES.get(propertyType);
String useDefaultType = property instanceof RegularProperty ?
ReactProp.USE_DEFAULT_TYPE : ReactPropGroup.USE_DEFAULT_TYPE;
String useDefaultType =
property instanceof RegularProperty
? ReactProp.USE_DEFAULT_TYPE
: ReactPropGroup.USE_DEFAULT_TYPE;
return useDefaultType.equals(property.customType()) ? defaultType : property.customType();
}
private static void checkElement(Element element) throws ReactPropertyException {
if (element.getKind() == ElementKind.METHOD
&& element.getModifiers().contains(PUBLIC)) {
if (element.getKind() == ElementKind.METHOD && element.getModifiers().contains(PUBLIC)) {
return;
}
throw new ReactPropertyException(
"@ReactProp and @ReachPropGroup annotation must be on a public method",
element);
"@ReactProp and @ReachPropGroup annotation must be on a public method", element);
}
private static boolean shouldIgnoreClass(ClassInfo classInfo) {
@ -464,10 +457,15 @@ public class ReactPropertyProcessor extends AbstractProcessor {
private interface Property {
String name();
String customType();
double defaultDouble();
float defaultFloat();
int defaultInt();
boolean defaultBoolean();
}
@ -575,9 +573,13 @@ public class ReactPropertyProcessor extends AbstractProcessor {
String name = propertyInfo.mProperty.name();
if (checkPropertyExists(name)) {
throw new ReactPropertyException(
"Module " + mClassName + " has already registered a property named \"" +
name + "\". If you want to override a property, don't add" +
"the @ReactProp annotation to the property in the subclass", propertyInfo);
"Module "
+ mClassName
+ " has already registered a property named \""
+ name
+ "\". If you want to override a property, don't add"
+ "the @ReactProp annotation to the property in the subclass",
propertyInfo);
}
mProperties.add(propertyInfo);
@ -601,10 +603,7 @@ public class ReactPropertyProcessor extends AbstractProcessor {
public final Property mProperty;
private PropertyInfo(
String methodName,
TypeName propertyType,
Element element,
Property property) {
String methodName, TypeName propertyType, Element element, Property property) {
this.methodName = methodName;
this.propertyType = propertyType;
this.element = element;
@ -622,8 +621,7 @@ public class ReactPropertyProcessor extends AbstractProcessor {
mClassInfo = classInfo;
}
public PropertyInfo build(Element element, Property property)
throws ReactPropertyException {
public PropertyInfo build(Element element, Property property) throws ReactPropertyException {
String methodName = element.getSimpleName().toString();
ExecutableElement method = (ExecutableElement) element;
@ -645,16 +643,14 @@ public class ReactPropertyProcessor extends AbstractProcessor {
TypeName indexType = TypeName.get(parameters.get(index++).asType());
if (!indexType.equals(TypeName.INT)) {
throw new ReactPropertyException(
"Argument " + index + " must be an int for @ReactPropGroup",
element);
"Argument " + index + " must be an int for @ReactPropGroup", element);
}
}
TypeName propertyType = TypeName.get(parameters.get(index++).asType());
if (!DEFAULT_TYPES.containsKey(propertyType)) {
throw new ReactPropertyException(
"Argument " + index + " must be of a supported type",
element);
"Argument " + index + " must be of a supported type", element);
}
return new PropertyInfo(methodName, propertyType, element, property);

Просмотреть файл

@ -6,25 +6,28 @@
package com.facebook.react.uimanager;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import android.view.View;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
public class ViewManagerPropertyUpdater {
public interface Settable {
void getProperties(Map<String, String> props);
void getProperties(Map<String, String> props);
}
public interface ViewManagerSetter<T extends ViewManager, V extends View> extends Settable {
void setProperty(T manager, V view, String name, ReactStylesDiffMap props);
void setProperty(T manager, V view, String name, Object value);
}
public interface ShadowNodeSetter<T extends ReactShadowNode> extends Settable {
void setProperty(T node, String name, ReactStylesDiffMap props);
void setProperty(T node, String name, Object value);
}
private static final String TAG = "ViewManagerPropertyUpdater";
@ -40,25 +43,21 @@ public class ViewManagerPropertyUpdater {
}
public static <T extends ViewManager, V extends View> void updateProps(
T manager,
V v,
ReactStylesDiffMap props) {
T manager, V v, ReactStylesDiffMap props) {
ViewManagerSetter<T, V> setter = findManagerSetter(manager.getClass());
ReadableMap propMap = props.mBackingMap;
ReadableMapKeySetIterator iterator = propMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
setter.setProperty(manager, v, key, props);
Iterator<Map.Entry<String, Object>> iterator = props.mBackingMap.getEntryIterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
setter.setProperty(manager, v, entry.getKey(), entry.getValue());
}
}
public static <T extends ReactShadowNode> void updateProps(T node, ReactStylesDiffMap props) {
ShadowNodeSetter<T> setter = findNodeSetter(node.getClass());
ReadableMap propMap = props.mBackingMap;
ReadableMapKeySetIterator iterator = propMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
setter.setProperty(node, key, props);
Iterator<Map.Entry<String, Object>> iterator = props.mBackingMap.getEntryIterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
setter.setProperty(node, entry.getKey(), entry.getValue());
}
}
@ -126,10 +125,10 @@ public class ViewManagerPropertyUpdater {
}
@Override
public void setProperty(T manager, V v, String name, ReactStylesDiffMap props) {
public void setProperty(T manager, V v, String name, Object value) {
ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name);
if (setter != null) {
setter.updateViewProp(manager, v, props);
setter.updateViewProp(manager, v, value);
}
}
@ -151,10 +150,10 @@ public class ViewManagerPropertyUpdater {
}
@Override
public void setProperty(ReactShadowNode node, String name, ReactStylesDiffMap props) {
public void setProperty(ReactShadowNode node, String name, Object value) {
ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name);
if (setter != null) {
setter.updateShadowNodeProp(node, props);
setter.updateShadowNodeProp(node, value);
}
}

Просмотреть файл

@ -8,6 +8,7 @@ package com.facebook.react.uimanager;
import android.view.View;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.DynamicFromObject;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
@ -33,7 +34,7 @@ import javax.annotation.Nullable;
EMPTY_PROPS_MAP.clear();
}
/*package*/ static abstract class PropSetter {
/*package*/ abstract static class PropSetter {
protected final String mPropName;
protected final String mPropType;
@ -50,16 +51,18 @@ import javax.annotation.Nullable;
private PropSetter(ReactProp prop, String defaultType, Method setter) {
mPropName = prop.name();
mPropType = ReactProp.USE_DEFAULT_TYPE.equals(prop.customType()) ?
defaultType : prop.customType();
mPropType =
ReactProp.USE_DEFAULT_TYPE.equals(prop.customType()) ? defaultType : prop.customType();
mSetter = setter;
mIndex = null;
}
private PropSetter(ReactPropGroup prop, String defaultType, Method setter, int index) {
mPropName = prop.names()[index];
mPropType = ReactPropGroup.USE_DEFAULT_TYPE.equals(prop.customType()) ?
defaultType : prop.customType();
mPropType =
ReactPropGroup.USE_DEFAULT_TYPE.equals(prop.customType())
? defaultType
: prop.customType();
mSetter = setter;
mIndex = index;
}
@ -72,52 +75,55 @@ import javax.annotation.Nullable;
return mPropType;
}
public void updateViewProp(
ViewManager viewManager,
View viewToUpdate,
ReactStylesDiffMap props) {
public void updateViewProp(ViewManager viewManager, View viewToUpdate, Object value) {
try {
if (mIndex == null) {
VIEW_MGR_ARGS[0] = viewToUpdate;
VIEW_MGR_ARGS[1] = extractProperty(props);
VIEW_MGR_ARGS[1] = getValueOrDefault(value);
mSetter.invoke(viewManager, VIEW_MGR_ARGS);
Arrays.fill(VIEW_MGR_ARGS, null);
} else {
VIEW_MGR_GROUP_ARGS[0] = viewToUpdate;
VIEW_MGR_GROUP_ARGS[1] = mIndex;
VIEW_MGR_GROUP_ARGS[2] = extractProperty(props);
VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value);
mSetter.invoke(viewManager, VIEW_MGR_GROUP_ARGS);
Arrays.fill(VIEW_MGR_GROUP_ARGS, null);
}
} catch (Throwable t) {
FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t);
throw new JSApplicationIllegalArgumentException("Error while updating property '" +
mPropName + "' of a view managed by: " + viewManager.getName(), t);
throw new JSApplicationIllegalArgumentException(
"Error while updating property '"
+ mPropName
+ "' of a view managed by: "
+ viewManager.getName(),
t);
}
}
public void updateShadowNodeProp(
ReactShadowNode nodeToUpdate,
ReactStylesDiffMap props) {
public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) {
try {
if (mIndex == null) {
SHADOW_ARGS[0] = extractProperty(props);
SHADOW_ARGS[0] = getValueOrDefault(value);
mSetter.invoke(nodeToUpdate, SHADOW_ARGS);
Arrays.fill(SHADOW_ARGS, null);
} else {
SHADOW_GROUP_ARGS[0] = mIndex;
SHADOW_GROUP_ARGS[1] = extractProperty(props);
SHADOW_GROUP_ARGS[1] = getValueOrDefault(value);
mSetter.invoke(nodeToUpdate, SHADOW_GROUP_ARGS);
Arrays.fill(SHADOW_GROUP_ARGS, null);
}
} catch (Throwable t) {
FLog.e(ViewManager.class, "Error while updating prop " + mPropName, t);
throw new JSApplicationIllegalArgumentException("Error while updating property '" +
mPropName + "' in shadow node of type: " + nodeToUpdate.getViewClass(), t);
throw new JSApplicationIllegalArgumentException(
"Error while updating property '"
+ mPropName
+ "' in shadow node of type: "
+ nodeToUpdate.getViewClass(),
t);
}
}
protected abstract @Nullable Object extractProperty(ReactStylesDiffMap props);
protected abstract @Nullable Object getValueOrDefault(Object value);
}
private static class DynamicPropSetter extends PropSetter {
@ -131,8 +137,12 @@ import javax.annotation.Nullable;
}
@Override
protected Object extractProperty(ReactStylesDiffMap props) {
return props.getDynamic(mPropName);
protected Object getValueOrDefault(Object value) {
if (value instanceof Dynamic) {
return value;
} else {
return new DynamicFromObject(value);
}
}
}
@ -151,8 +161,9 @@ import javax.annotation.Nullable;
}
@Override
protected Object extractProperty(ReactStylesDiffMap props) {
return props.getInt(mPropName, mDefaultValue);
protected Object getValueOrDefault(Object value) {
// All numbers from JS are Doubles which can't be simply cast to Integer
return value == null ? mDefaultValue : (Integer) ((Double)value).intValue();
}
}
@ -171,8 +182,8 @@ import javax.annotation.Nullable;
}
@Override
protected Object extractProperty(ReactStylesDiffMap props) {
return props.getDouble(mPropName, mDefaultValue);
protected Object getValueOrDefault(Object value) {
return value == null ? mDefaultValue : (Double) value;
}
}
@ -186,8 +197,9 @@ import javax.annotation.Nullable;
}
@Override
protected Object extractProperty(ReactStylesDiffMap props) {
return props.getBoolean(mPropName, mDefaultValue) ? Boolean.TRUE : Boolean.FALSE;
protected Object getValueOrDefault(Object value) {
boolean val = value == null ? mDefaultValue : (boolean) value;
return val ? Boolean.TRUE : Boolean.FALSE;
}
}
@ -206,8 +218,9 @@ import javax.annotation.Nullable;
}
@Override
protected Object extractProperty(ReactStylesDiffMap props) {
return props.getFloat(mPropName, mDefaultValue);
protected Object getValueOrDefault(Object value) {
// All numbers from JS are Doubles which can't be simply cast to Float
return value == null ? mDefaultValue : (Float) ((Double)value).floatValue();
}
}
@ -218,8 +231,8 @@ import javax.annotation.Nullable;
}
@Override
protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
return props.getArray(mPropName);
protected @Nullable Object getValueOrDefault(Object value) {
return (ReadableArray) value;
}
}
@ -230,8 +243,8 @@ import javax.annotation.Nullable;
}
@Override
protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
return props.getMap(mPropName);
protected @Nullable Object getValueOrDefault(Object value) {
return (ReadableMap) value;
}
}
@ -242,8 +255,8 @@ import javax.annotation.Nullable;
}
@Override
protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
return props.getString(mPropName);
protected @Nullable Object getValueOrDefault(Object value) {
return (String) value;
}
}
@ -254,9 +267,9 @@ import javax.annotation.Nullable;
}
@Override
protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
if (!props.isNull(mPropName)) {
return props.getBoolean(mPropName, /* ignored */ false) ? Boolean.TRUE : Boolean.FALSE;
protected @Nullable Object getValueOrDefault(Object value) {
if (value != null) {
return (boolean) value ? Boolean.TRUE : Boolean.FALSE;
}
return null;
}
@ -273,9 +286,9 @@ import javax.annotation.Nullable;
}
@Override
protected @Nullable Object extractProperty(ReactStylesDiffMap props) {
if (!props.isNull(mPropName)) {
return props.getInt(mPropName, /* ignored */ 0);
protected @Nullable Object getValueOrDefault(Object value) {
if (value != null) {
return (Integer) value;
}
return null;
}
@ -317,9 +330,10 @@ import javax.annotation.Nullable;
}
// This is to include all the setters from parent classes. Once calculated the result will be
// stored in CLASS_PROPS_CACHE so that we only scan for @ReactProp annotations once per class.
props = new HashMap<>(
getNativePropSettersForViewManagerClass(
(Class<? extends ViewManager>) cls.getSuperclass()));
props =
new HashMap<>(
getNativePropSettersForViewManagerClass(
(Class<? extends ViewManager>) cls.getSuperclass()));
extractPropSettersFromViewManagerClassDefinition(cls, props);
CLASS_PROPS_CACHE.put(cls, props);
return props;
@ -343,18 +357,17 @@ import javax.annotation.Nullable;
return props;
}
// This is to include all the setters from parent classes up to ReactShadowNode class
props = new HashMap<>(
getNativePropSettersForShadowNodeClass(
(Class<? extends ReactShadowNode>) cls.getSuperclass()));
props =
new HashMap<>(
getNativePropSettersForShadowNodeClass(
(Class<? extends ReactShadowNode>) cls.getSuperclass()));
extractPropSettersFromShadowNodeClassDefinition(cls, props);
CLASS_PROPS_CACHE.put(cls, props);
return props;
}
private static PropSetter createPropSetter(
ReactProp annotation,
Method method,
Class<?> propTypeClass) {
ReactProp annotation, Method method, Class<?> propTypeClass) {
if (propTypeClass == Dynamic.class) {
return new DynamicPropSetter(annotation, method);
} else if (propTypeClass == boolean.class) {
@ -376,8 +389,13 @@ import javax.annotation.Nullable;
} else if (propTypeClass == ReadableMap.class) {
return new MapPropSetter(annotation, method);
} else {
throw new RuntimeException("Unrecognized type: " + propTypeClass + " for method: " +
method.getDeclaringClass().getName() + "#" + method.getName());
throw new RuntimeException(
"Unrecognized type: "
+ propTypeClass
+ " for method: "
+ method.getDeclaringClass().getName()
+ "#"
+ method.getName());
}
}
@ -389,43 +407,38 @@ import javax.annotation.Nullable;
String[] names = annotation.names();
if (propTypeClass == Dynamic.class) {
for (int i = 0; i < names.length; i++) {
props.put(
names[i],
new DynamicPropSetter(annotation, method, i));
props.put(names[i], new DynamicPropSetter(annotation, method, i));
}
} else if (propTypeClass == int.class) {
for (int i = 0; i < names.length; i++) {
props.put(
names[i],
new IntPropSetter(annotation, method, i, annotation.defaultInt()));
props.put(names[i], new IntPropSetter(annotation, method, i, annotation.defaultInt()));
}
} else if (propTypeClass == float.class) {
for (int i = 0; i < names.length; i++) {
props.put(
names[i],
new FloatPropSetter(annotation, method, i, annotation.defaultFloat()));
props.put(names[i], new FloatPropSetter(annotation, method, i, annotation.defaultFloat()));
}
} else if (propTypeClass == double.class) {
for (int i = 0; i < names.length; i++) {
props.put(
names[i],
new DoublePropSetter(annotation, method, i, annotation.defaultDouble()));
names[i], new DoublePropSetter(annotation, method, i, annotation.defaultDouble()));
}
} else if (propTypeClass == Integer.class) {
for (int i = 0; i < names.length; i++) {
props.put(
names[i],
new BoxedIntPropSetter(annotation, method, i));
props.put(names[i], new BoxedIntPropSetter(annotation, method, i));
}
} else {
throw new RuntimeException("Unrecognized type: " + propTypeClass + " for method: " +
method.getDeclaringClass().getName() + "#" + method.getName());
throw new RuntimeException(
"Unrecognized type: "
+ propTypeClass
+ " for method: "
+ method.getDeclaringClass().getName()
+ "#"
+ method.getName());
}
}
private static void extractPropSettersFromViewManagerClassDefinition(
Class<? extends ViewManager> cls,
Map<String, PropSetter> props) {
Class<? extends ViewManager> cls, Map<String, PropSetter> props) {
Method[] declaredMethods = cls.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
Method method = declaredMethods[i];
@ -433,30 +446,42 @@ import javax.annotation.Nullable;
if (annotation != null) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 2) {
throw new RuntimeException("Wrong number of args for prop setter: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Wrong number of args for prop setter: " + cls.getName() + "#" + method.getName());
}
if (!View.class.isAssignableFrom(paramTypes[0])) {
throw new RuntimeException("First param should be a view subclass to be updated: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"First param should be a view subclass to be updated: "
+ cls.getName()
+ "#"
+ method.getName());
}
props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[1]));
}
ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class);
if (groupAnnotation != null) {
Class<?> [] paramTypes = method.getParameterTypes();
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 3) {
throw new RuntimeException("Wrong number of args for group prop setter: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Wrong number of args for group prop setter: "
+ cls.getName()
+ "#"
+ method.getName());
}
if (!View.class.isAssignableFrom(paramTypes[0])) {
throw new RuntimeException("First param should be a view subclass to be updated: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"First param should be a view subclass to be updated: "
+ cls.getName()
+ "#"
+ method.getName());
}
if (paramTypes[1] != int.class) {
throw new RuntimeException("Second argument should be property index: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Second argument should be property index: "
+ cls.getName()
+ "#"
+ method.getName());
}
createPropSetters(groupAnnotation, method, paramTypes[2], props);
}
@ -464,29 +489,34 @@ import javax.annotation.Nullable;
}
private static void extractPropSettersFromShadowNodeClassDefinition(
Class<? extends ReactShadowNode> cls,
Map<String, PropSetter> props) {
Class<? extends ReactShadowNode> cls, Map<String, PropSetter> props) {
for (Method method : cls.getDeclaredMethods()) {
ReactProp annotation = method.getAnnotation(ReactProp.class);
if (annotation != null) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 1) {
throw new RuntimeException("Wrong number of args for prop setter: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Wrong number of args for prop setter: " + cls.getName() + "#" + method.getName());
}
props.put(annotation.name(), createPropSetter(annotation, method, paramTypes[0]));
}
ReactPropGroup groupAnnotation = method.getAnnotation(ReactPropGroup.class);
if (groupAnnotation != null) {
Class<?> [] paramTypes = method.getParameterTypes();
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 2) {
throw new RuntimeException("Wrong number of args for group prop setter: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Wrong number of args for group prop setter: "
+ cls.getName()
+ "#"
+ method.getName());
}
if (paramTypes[0] != int.class) {
throw new RuntimeException("Second argument should be property index: " +
cls.getName() + "#" + method.getName());
throw new RuntimeException(
"Second argument should be property index: "
+ cls.getName()
+ "#"
+ method.getName());
}
createPropSetters(groupAnnotation, method, paramTypes[1], props);
}

Просмотреть файл

@ -9,6 +9,7 @@ package com.facebook.react.uimanager;
import java.util.Map;
import android.graphics.drawable.ColorDrawable;
import android.view.View;
import com.facebook.react.bridge.ReadableMap;
@ -94,6 +95,20 @@ public class SimpleViewPropertyTest {
assertThat(view.getAlpha()).isEqualTo(1.0f);
}
@Test
public void testBackgroundColor() {
View view = mManager.createView(mThemedContext, new JSResponderHandler());
mManager.updateProperties(view, buildStyles());
assertThat(view.getBackground()).isEqualTo(null);
mManager.updateProperties(view, buildStyles("backgroundColor", 12));
assertThat(((ColorDrawable)view.getBackground()).getColor()).isEqualTo(12);
mManager.updateProperties(view, buildStyles("backgroundColor", null));
assertThat(((ColorDrawable)view.getBackground()).getColor()).isEqualTo(0);
}
@Test
public void testGetNativeProps() {
Map<String, String> nativeProps = mManager.getNativeProps();