diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java index faf123e88..ad491ab50 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/JSApplicationIllegalArgumentException.java @@ -17,4 +17,8 @@ public class JSApplicationIllegalArgumentException extends JSApplicationCausedNa public JSApplicationIllegalArgumentException(String detailMessage) { super(detailMessage); } + + public JSApplicationIllegalArgumentException(String detailMessage, Throwable t) { + super(detailMessage, t); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 01b220102..035560322 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -93,7 +93,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; throw new IllegalViewOperationException("Trying to update view with tag " + tag + " which doesn't exist"); } - viewManager.updateView(viewToUpdate, props); + viewManager.updateProperties(viewToUpdate, props); } public void updateViewExtraData(int tag, Object extraData) { @@ -179,7 +179,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; // creating another (potentially much more expensive) mapping from view to React tag view.setId(tag); if (initialProps != null) { - viewManager.updateView(view, initialProps); + viewManager.updateProperties(view, initialProps); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactProp.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactProp.java new file mode 100644 index 000000000..d387fa71b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactProp.java @@ -0,0 +1,93 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Use this annotation to annotate properties of native views that should be exposed to JS. This + * annotation should only be used for setter methods of subclasses of {@link ViewManager}. + * + * Each annotated method should return {@code void} and take exactly two arguments: first being + * a view instance to be updated and second a value that should be set. + * + * Allowed types of values are: + * - primitives (int, boolean, double, float) + * - {@link String} + * - {@link Boolean} + * - {@link ReadableArray} + * - {@link ReadableMap} + * + * When property gets removed from the corresponding component in React, annotated setter will be + * called with {@code null} in case of non-primitive value type or with a default value in case when + * the value type is a primitive (use appropriate default field of this annotation to customize + * default value that is going to be used: {@link #defaultBoolean}, {@link #defaultDouble}, etc.) + * + * Since in case of property removal for non-primitive value type setter will be called with value + * set to {@code null} it's required that value type is annotated with {@link Nullable}. + * + * Note: Since boolean property type can be represented both as primitive and wrapped default value + * set through {@link #defaultBoolean} is only respected for primitive type and for the wrapped type + * {@code null} will be used as a default. + */ +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface ReactProp { + + // Used as a default value for "customType" property as "null" is not allowed. Moreover, when this + // const is used in annotation declaration compiler will actually create a copy of it, so + // comparing it using "==" with this filed doesn't work either. We need to compare using "equals" + // which means that this value needs to be unique. + String USE_DEFAULT_TYPE = "__default_type__"; + + /** + * Name of the property exposed to JS that will be updated using setter method annotated with + * the given instance of {@code ReactProp} annotation + */ + String name(); + + /** + * Type of property that will be send to JS. In most of the cases {@code customType} should not be + * set in which case default type will be send to JS based on the type of value argument from the + * setter method (e.g. for {@code int}, {@code double} default is "number", for + * {@code ReadableArray} it's "Array"). Custom type may be used when additional processing of the + * value needs to be done in JS before sending it over the brige. A good example of that would be + * backgroundColor property, which is expressed as a {@code String} in JS, but we use + * {@code processColor} JS module to convert it to {@code int} before sending over the bridge. + */ + @Nullable String customType() default USE_DEFAULT_TYPE; + + /** + * Default value for property of type {@code double}. This value will be provided to property + * setter method annotated with {@link ReactProp} if property with a given name gets removed + * from the component description in JS + */ + double defaultDouble() default 0.0; + + /** + * Default value for property of type {@code float}. This value will be provided to property + * setter method annotated with {@link ReactProp} if property with a given name gets removed + * from the component description in JS + */ + float defaultFloat() default 0.0f; + + /** + * Default value for property of type {@code int}. This value will be provided to property + * setter method annotated with {@link ReactProp} if property with a given name gets removed + * from the component description in JS + */ + int defaultInt() default 0; + + /** + * Default value for property of type {@code boolean}. This value will be provided to property + * setter method annotated with {@link ReactProp} if property with a given name gets removed + * from the component description in JS + */ + boolean defaultBoolean() default false; +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPropGroup.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPropGroup.java new file mode 100644 index 000000000..13c843314 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactPropGroup.java @@ -0,0 +1,77 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager; + +import javax.annotation.Nullable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Use this annotation to annotate group of properties of native views that should be exposed to JS. + * This annotation should only be used for setter methods of subclasses of {@link ViewManager}. It's + * a batched version of {@link ReactProp} annotation (please see documentation of {@link ReactProp} + * for more details about how this annotation can be used). + * + * This annotation is meant to be used for a group of similar properties. That's why it support only + * a set of properties of the same type. A good example is supporting "border", where we have 7 + * variations of that property ("borderLeft", "borderHorizontal", etc.) and very similar code for + * handling each of those. + * + * Each annotated method should return {@code void} and take exactly three arguments: first being + * a view instance to be updated, second should be of type int and will represent index in the + * group of the property being updated. Last, third argument represent the value that should be set. + * + * + * Currently only {@code int}, {@code float} and {@link String} value types are supported. + * + * In case when property has been removed from the corresponding react component annotated setter + * will be called and default value will be provided as a value parameter. Default value can be + * customize using {@link #defaultInt} or {@link #defaultFloat} in the case when property is of + * one of primitive types. In case when {@link String} is the property type {@code null} value will + * be provided as a default. + */ +@Retention(RUNTIME) +@Target(ElementType.METHOD) +public @interface ReactPropGroup { + + // Used as a default value for "customType" property as "null" is not allowed. Moreover, when this + // const is used in annotation declaration compiler will actually create a copy of it, so + // comparing it using "==" with this filed doesn't work either. We need to compare using "equals" + // which means that this value needs to be unique. + String USE_DEFAULT_TYPE = "__default_type__"; + + /** + * Array of names of properties exposed to JS that will be updated using setter method annotated + * with the given instance of {@code ReactPropGroup} annotation + */ + String[] names(); + + /** + * Type of property that will be send to JS. In most of the cases {@code customType} should not be + * set in which case default type will be send to JS based on the type of value argument from the + * setter method (e.g. for {@code int}, {@code float} default is "number"). Custom type may be + * used when additional processing of the value needs to be done in JS before sending it over the + * bridge. A good example of that would be backgroundColor property, which is expressed as a + * {@code String} in JS, but we use {@code processColor} JS module to convert it to {@code int} + * before sending over the bridge. + */ + @Nullable String customType() default USE_DEFAULT_TYPE; + + /** + * Default value for property of type {@code float}. This value will be provided to property + * setter method annotated with {@link ReactPropGroup} if property with a given name gets removed + * from the component description in JS + */ + float defaultFloat() default 0.0f; + + /** + * Default value for property of type {@code int}. This value will be provided to property + * setter method annotated with {@link ReactPropGroup} if property with a given name gets removed + * from the component description in JS + */ + int defaultInt() default 0; +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java index 61ca838c2..bce7c3736 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -64,13 +64,9 @@ import com.facebook.react.common.MapBuilder; if (viewManagerCommands != null) { viewManagerConstants.put("Commands", viewManagerCommands); } - Map viewManagerNativeProps = viewManager.getNativeProps(); + Map viewManagerNativeProps = viewManager.getNativeProps(); if (!viewManagerNativeProps.isEmpty()) { - Map nativeProps = new HashMap<>(); - for (Map.Entry entry : viewManagerNativeProps.entrySet()) { - nativeProps.put(entry.getKey(), entry.getValue().toString()); - } - viewManagerConstants.put("NativeProps", nativeProps); + viewManagerConstants.put("NativeProps", viewManagerNativeProps); } if (!viewManagerConstants.isEmpty()) { constants.put(viewManager.getName(), viewManagerConstants); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java index eaf442d23..230f4cb51 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -18,10 +18,11 @@ import java.util.Map; import android.view.View; import com.facebook.csslayout.CSSNode; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySeyIterator; import com.facebook.react.touch.CatalystInterceptingViewGroup; import com.facebook.react.touch.JSResponderHandler; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableArray; /** * Class responsible for knowing how to create and update catalyst Views of a given type. It is also @@ -32,6 +33,28 @@ public abstract class ViewManager { private static final Map> CLASS_PROP_CACHE = new HashMap<>(); + public final void updateProperties(T viewToUpdate, CatalystStylesDiffMap props) { + Map propSetters = + ViewManagersPropertyCache.getNativePropSettersForClass(getClass()); + ReadableMap propMap = props.mBackingMap; + ReadableMapKeySeyIterator iterator = propMap.keySetIterator(); + // TODO(krzysztof): Remove missingSetters code once all views are migrated to @ReactProp + boolean missingSetters = false; + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); + if (setter != null) { + setter.updateProp(this, viewToUpdate, props); + } else { + missingSetters = true; + } + } + if (missingSetters) { + updateView(viewToUpdate, props); + } + onAfterUpdateTransaction(viewToUpdate); + } + /** * Creates a view and installs event emitters on it. */ @@ -84,8 +107,22 @@ public abstract class ViewManager { * Subclass should use this method to populate native view with updated style properties. In case * when a certain property is present in {@param props} map but the value is null, this property * should be reset to the default value + * + * TODO(krzysztof) This method should be replaced by updateProperties and removed completely after + * all view managers adapt @ReactProp */ - public abstract void updateView(T root, CatalystStylesDiffMap props); + @Deprecated + protected void updateView(T root, CatalystStylesDiffMap props) { + } + + /** + * Callback that will be triggered after all properties are updated in current update transaction + * (all @ReactProp handlers for properties updated in current transaction have been called). If + * you want to override this method you should call super.onAfterUpdateTransaction from it as + * the parent class of the ViewManager may rely on callback being executed. + */ + protected void onAfterUpdateTransaction(T view) { + } /** * Subclasses can implement this method to receive an optional extra data enqueued from the @@ -178,13 +215,15 @@ public abstract class ViewManager { return null; } - public Map getNativeProps() { - Map nativeProps = new HashMap<>(); + public Map getNativeProps() { + // TODO(krzysztof): This method will just delegate to ViewManagersPropertyRegistry once + // refactoring is finished Class cls = getClass(); + Map nativeProps = ViewManagersPropertyCache.getNativePropsForClass(cls); while (cls.getSuperclass() != null) { Map props = getNativePropsForClass(cls); for (Map.Entry entry : props.entrySet()) { - nativeProps.put(entry.getKey(), entry.getValue()); + nativeProps.put(entry.getKey(), entry.getValue().toString()); } cls = cls.getSuperclass(); } @@ -192,6 +231,7 @@ public abstract class ViewManager { } private Map getNativePropsForClass(Class cls) { + // TODO(krzysztof): Blow up this method once refactoring is finished Map props = CLASS_PROP_CACHE.get(cls); if (props != null) { return props; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java new file mode 100644 index 000000000..1b3144a7e --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java @@ -0,0 +1,405 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import android.view.View; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; + +/** + * This class is responsible for holding view manager property setters and is used in a process of + * updating views with the new properties set in JS. + */ +/*package*/ class ViewManagersPropertyCache { + + private static final Map> CLASS_PROPS_CACHE = new HashMap<>(); + private static final Map EMPTY_PROPS_MAP = new HashMap<>(); + + /*package*/ static abstract class PropSetter { + + protected final String mPropName; + protected final String mPropType; + protected final Method mSetter; + + // The following two constructors make it easy to reuse code responsible for setting property + // type. It's probably not a best design but this API is not exposed and since we can't use + // inheritance for annotation classes it's the easiest way to avoid creating an extra base class + // just to support group and non-group setters. + private PropSetter(ReactProp prop, String defaultType, Method setter) { + mPropName = prop.name(); + mPropType = ReactProp.USE_DEFAULT_TYPE.equals(prop.customType()) ? + defaultType : prop.customType(); + mSetter = setter; + } + + 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(); + mSetter = setter; + } + + public String getPropName() { + return mPropName; + } + + public String getPropType() { + return mPropType; + } + + public void updateProp( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) { + try { + updateProperty(viewManager, viewToUpdate, props); + } 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); + } + } + + protected abstract void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException; + } + + private static class IntPropSetter extends PropSetter { + + private final int mDefaultValue; + + public IntPropSetter(ReactProp prop, Method setter, int defaultValue) { + super(prop, "number", setter); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getInt(mPropName, mDefaultValue)); + } + } + + private static class DoublePropSetter extends PropSetter { + + private final double mDefaultValue; + + public DoublePropSetter(ReactProp prop, Method setter, double defaultValue) { + super(prop, "number", setter); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getDouble(mPropName, mDefaultValue)); + } + } + + private static class BooleanPropSetter extends PropSetter { + + private final boolean mDefaultValue; + + public BooleanPropSetter(ReactProp prop, Method setter, boolean defaultValue) { + super(prop, "boolean", setter); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getBoolean(mPropName, mDefaultValue)); + } + } + + private static class FloatPropSetter extends PropSetter { + + private final float mDefaultValue; + + public FloatPropSetter(ReactProp prop, Method setter, float defaultValue) { + super(prop, "number", setter); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getFloat(mPropName, mDefaultValue)); + } + } + + private static class ArrayPropSetter extends PropSetter { + + public ArrayPropSetter(ReactProp prop, Method setter) { + super(prop, "Array", setter); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getArray(mPropName)); + } + } + + private static class MapPropSetter extends PropSetter { + + public MapPropSetter(ReactProp prop, Method setter) { + super(prop, "Map", setter); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getMap(mPropName)); + } + } + + private static class StringPropSetter extends PropSetter { + + public StringPropSetter(ReactProp prop, Method setter) { + super(prop, "String", setter); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, props.getString(mPropName)); + } + } + + private static class BoxedBooleanPropSetter extends PropSetter { + + public BoxedBooleanPropSetter(ReactProp prop, Method setter) { + super(prop, "boolean", setter); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + Boolean value = null; + if (!props.isNull(mPropName)) { + value = props.getBoolean(mPropName, false) ? Boolean.TRUE : Boolean.FALSE; + } + mSetter.invoke(viewManager, viewToUpdate, value); + } + } + + private static class BoxedIntPropSetter extends PropSetter { + + public BoxedIntPropSetter(ReactProp prop, Method setter) { + super(prop, "number", setter); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + Integer value = null; + if (!props.isNull(mPropName)) { + value = props.getInt(mPropName, /* ignored */ 0); + } + mSetter.invoke(viewManager, viewToUpdate, value); + } + } + + private static abstract class GroupSetter extends PropSetter { + + protected final int mIndex; + + protected GroupSetter(ReactPropGroup prop, String defaultType, Method setter, int index) { + super(prop, defaultType, setter, index); + mIndex = index; + } + } + + private static class GroupIntSetter extends GroupSetter { + + private final int mDefaultValue; + + public GroupIntSetter(ReactPropGroup prop, Method setter, int index, int defaultValue) { + super(prop, "number", setter, index); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, mIndex, props.getInt(mPropName, mDefaultValue)); + } + } + + private static class GroupFloatSetter extends GroupSetter { + + private final float mDefaultValue; + + public GroupFloatSetter(ReactPropGroup prop, Method setter, int index, float defaultValue) { + super(prop, "number", setter, index); + mDefaultValue = defaultValue; + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke(viewManager, viewToUpdate, mIndex, props.getFloat(mPropName, mDefaultValue)); + } + } + + private static class GroupBoxedIntSetter extends GroupSetter { + + protected GroupBoxedIntSetter(ReactPropGroup prop, Method setter, int index) { + super(prop, "number", setter, index); + } + + @Override + protected void updateProperty( + ViewManager viewManager, + View viewToUpdate, + CatalystStylesDiffMap props) throws InvocationTargetException, IllegalAccessException { + mSetter.invoke( + viewManager, + viewToUpdate, + mIndex, + props.isNull(mPropName) ? null : props.getInt(mPropName, /* unused */ 0)); + } + } + + /*package*/ static Map getNativePropsForClass( + Class topLevelClass) { + Map nativeProps = new HashMap<>(); + Map props = getNativePropSettersForClass(topLevelClass); + for (PropSetter setter : props.values()) { + nativeProps.put(setter.getPropName(), setter.getPropType()); + } + return nativeProps; + } + + /*package*/ static Map getNativePropSettersForClass( + Class cls) { + if (cls == ViewManager.class) { + return EMPTY_PROPS_MAP; + } + Map props = CLASS_PROPS_CACHE.get(cls); + if (props != null) { + return props; + } + props = new HashMap<>( + getNativePropSettersForClass((Class) cls.getSuperclass())); + for (Method method : cls.getDeclaredMethods()) { + { + ReactProp annotation = method.getAnnotation(ReactProp.class); + 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()); + } + if (!View.class.isAssignableFrom(paramTypes[0])) { + throw new RuntimeException("First param should be a view subclass to be updated: " + + cls.getName() + "#" + method.getName()); + } + Class propTypeClass = paramTypes[1]; + PropSetter propSetter; + if (propTypeClass == boolean.class) { + propSetter = + new BooleanPropSetter(annotation, method, annotation.defaultBoolean()); + } else if (propTypeClass == int.class) { + propSetter = new IntPropSetter(annotation, method, annotation.defaultInt()); + } else if (propTypeClass == float.class) { + propSetter = new FloatPropSetter(annotation, method, annotation.defaultFloat()); + } else if (propTypeClass == double.class) { + propSetter = + new DoublePropSetter(annotation, method, annotation.defaultDouble()); + } else if (propTypeClass == String.class) { + propSetter = new StringPropSetter(annotation, method); + } else if (propTypeClass == Boolean.class) { + propSetter = new BoxedBooleanPropSetter(annotation, method); + } else if (propTypeClass == Integer.class) { + propSetter = new BoxedIntPropSetter(annotation, method); + } else if (propTypeClass == ReadableArray.class) { + propSetter = new ArrayPropSetter(annotation, method); + } else if (propTypeClass == ReadableMap.class) { + propSetter = new MapPropSetter(annotation, method); + } else { + throw new RuntimeException("Unrecognized type"); + } + props.put(annotation.name(), propSetter); + } + } + { + ReactPropGroup annotation = method.getAnnotation(ReactPropGroup.class); + if (annotation != null) { + Class [] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 3) { + 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()); + } + if (paramTypes[1] != int.class) { + throw new RuntimeException("Second argument should be property index: " + + cls.getName() + "#" + method.getName()); + } + Class propTypeClass = paramTypes[2]; + String[] names = annotation.names(); + if (propTypeClass == int.class) { + for (int i = 0; i < names.length; i++) { + props.put( + names[i], + new GroupIntSetter(annotation, method, i, annotation.defaultInt())); + } + } else if (propTypeClass == float.class) { + for (int i = 0; i < names.length; i++) { + props.put( + names[i], + new GroupFloatSetter(annotation, method, i, annotation.defaultFloat())); + } + } else if (propTypeClass == Integer.class) { + for (int i = 0; i < names.length; i++) { + props.put( + names[i], + new GroupBoxedIntSetter(annotation, method, i)); + } + } else { + throw new RuntimeException("Unrecognized type: " + paramTypes[2] + " for method: " + + cls.getName() + "#" + method.getName()); + } + } + } + } + CLASS_PROPS_CACHE.put(cls, props); + return props; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 2b4c425d6..a316e799f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -69,15 +69,15 @@ public class ReactViewManager extends ViewGroupManager { } @Override - public Map getNativeProps() { - Map nativeProps = super.getNativeProps(); + public Map getNativeProps() { + Map nativeProps = super.getNativeProps(); Map baseProps = BaseViewPropertyApplicator.getCommonProps(); for (Map.Entry entry : baseProps.entrySet()) { - nativeProps.put(entry.getKey(), entry.getValue()); + nativeProps.put(entry.getKey(), entry.getValue().toString()); } for (int i = 0; i < SPACING_TYPES.length; i++) { - nativeProps.put(ViewProps.BORDER_WIDTHS[i], UIProp.Type.NUMBER); - nativeProps.put(PROPS_BORDER_COLOR[i], UIProp.Type.STRING); + nativeProps.put(ViewProps.BORDER_WIDTHS[i], UIProp.Type.NUMBER.toString()); + nativeProps.put(PROPS_BORDER_COLOR[i], UIProp.Type.STRING.toString()); } return nativeProps; }