diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/DynamicFromObject.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/DynamicFromObject.java new file mode 100644 index 00000000000..3f7f24bf02b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/DynamicFromObject.java @@ -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; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index f0fd43edd01..119890a2088 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -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. + *

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. + *

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> getEntryIterator() { + return mBackingMap.entrySet().iterator(); + } + @Override public @Nonnull ReadableMapKeySetIterator keySetIterator() { return new ReadableMapKeySetIterator() { - Iterator mIterator = mBackingMap.keySet().iterator(); + Iterator> 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(); } }; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java index 8a2ae68dbdb..a51e7c89d39 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java @@ -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. + *

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 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> getEntryIterator(); + + @Nonnull + ReadableMapKeySetIterator keySetIterator(); + + @Nonnull + HashMap toHashMap(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index 38fd926827c..cbd28320503 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -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. + *

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 mLocalMap; - private @Nullable HashMap mLocalTypeMap; + private @Nullable HashMap mLocalMap; + private @Nullable HashMap mLocalTypeMap; private static int mJniCallCounter; + public static void setUseNativeAccessor(boolean useNativeAccessor) { ReactFeatureFlags.useMapNativeAccessor = useNativeAccessor; } + public static int getJNIPassCounter() { return mJniCallCounter; } - private HashMap getLocalMap() { + private HashMap 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 getLocalTypeMap() { + private @Nonnull HashMap 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> 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(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java b/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java index f9742ac08d0..4ebe07c820f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java +++ b/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java @@ -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 $$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 $$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 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() { + classInfo.mProperties, + new Comparator() { @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 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 properties) { - MethodSpec.Builder builder = MethodSpec.methodBuilder("setProperty") - .addModifiers(PUBLIC) - .addAnnotation(Override.class) - .returns(TypeName.VOID); + ClassInfo classInfo, List 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); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java index 7bea9928a7c..6ab0cdb7a00 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java @@ -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 props); + void getProperties(Map props); } public interface ViewManagerSetter 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 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 void updateProps( - T manager, - V v, - ReactStylesDiffMap props) { + T manager, V v, ReactStylesDiffMap props) { ViewManagerSetter 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> iterator = props.mBackingMap.getEntryIterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + setter.setProperty(manager, v, entry.getKey(), entry.getValue()); } } public static void updateProps(T node, ReactStylesDiffMap props) { ShadowNodeSetter 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> iterator = props.mBackingMap.getEntryIterator(); + while (iterator.hasNext()) { + Map.Entry 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); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java index 98428f40b9f..f987fc2ef5b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java @@ -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) cls.getSuperclass())); + props = + new HashMap<>( + getNativePropSettersForViewManagerClass( + (Class) 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) cls.getSuperclass())); + props = + new HashMap<>( + getNativePropSettersForShadowNodeClass( + (Class) 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 cls, - Map props) { + Class cls, Map 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 cls, - Map props) { + Class cls, Map 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); } diff --git a/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java b/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java index dc67c8e72ab..95cc4d52767 100644 --- a/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java +++ b/ReactAndroid/src/test/java/com/facebook/react/uimanager/SimpleViewPropertyTest.java @@ -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 nativeProps = mManager.getNativeProps();