diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index a20c71937..9721ee4dd 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -227,6 +227,9 @@ android { jni.srcDirs = [] jniLibs.srcDir "$buildDir/react-ndk/exported" res.srcDirs = ['src/main/res/devsupport', 'src/main/res/shell'] + java { + exclude 'com/facebook/react/processing' + } } tasks.withType(JavaCompile) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java b/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java new file mode 100644 index 000000000..bee08f387 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/processing/ReactPropertyProcessor.java @@ -0,0 +1,691 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.processing; + +import javax.annotation.Nullable; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.facebook.infer.annotation.SuppressFieldNotInitialized; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.annotations.ReactPropertyHolder; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.annotations.ReactPropGroup; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; + +import static javax.lang.model.element.Modifier.*; +import static javax.tools.Diagnostic.Kind.ERROR; +import static javax.tools.Diagnostic.Kind.WARNING; + +/** + * 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 + * reflection. + */ +@SupportedAnnotationTypes("com.facebook.react.uimanager.annotations.ReactPropertyHolder") +@SupportedSourceVersion(SourceVersion.RELEASE_7) +public class ReactPropertyProcessor extends AbstractProcessor { + private static final Map DEFAULT_TYPES; + private static final Set BOXED_PRIMITIVES; + + private static final TypeName PROPS_TYPE = + ClassName.get("com.facebook.react.uimanager", "CatalystStylesDiffMap"); + 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 VIEW_MANAGER_TYPE = + ClassName.get("com.facebook.react.uimanager", "ViewManager"); + private static final TypeName SHADOW_NODE_TYPE = + ClassName.get("com.facebook.react.uimanager", "ReactShadowNode"); + + private static final ClassName VIEW_MANAGER_SETTER_TYPE = + ClassName.get( + "com.facebook.react.uimanager", + "ViewManagerPropertyUpdater", + "ViewManagerSetter"); + private static final ClassName SHADOW_NODE_SETTER_TYPE = + ClassName.get( + "com.facebook.react.uimanager", + "ViewManagerPropertyUpdater", + "ShadowNodeSetter"); + + private static final TypeName PROPERTY_MAP_TYPE = + ParameterizedTypeName.get(Map.class, String.class, String.class); + private static final TypeName CONCRETE_PROPERTY_MAP_TYPE = + ParameterizedTypeName.get(HashMap.class, String.class, String.class); + + private static final TypeName MAPPINGS_MAP_TYPE = + ParameterizedTypeName.get(Map.class, String.class, Integer.class); + private static final TypeName CONCRETE_MAPPINGS_MAP_TYPE = + ParameterizedTypeName.get(HashMap.class, String.class, Integer.class); + + private final Map mClasses; + + @SuppressFieldNotInitialized + private Filer mFiler; + @SuppressFieldNotInitialized + private Messager mMessager; + @SuppressFieldNotInitialized + private Elements mElements; + @SuppressFieldNotInitialized + private Types mTypes; + + static { + DEFAULT_TYPES = new HashMap<>(); + + // Primitives + DEFAULT_TYPES.put(TypeName.BOOLEAN, "boolean"); + DEFAULT_TYPES.put(TypeName.DOUBLE, "number"); + DEFAULT_TYPES.put(TypeName.FLOAT, "number"); + DEFAULT_TYPES.put(TypeName.INT, "number"); + + // Boxed primitives + DEFAULT_TYPES.put(TypeName.BOOLEAN.box(), "boolean"); + DEFAULT_TYPES.put(TypeName.INT.box(), "number"); + + // Class types + DEFAULT_TYPES.put(STRING_TYPE, "String"); + DEFAULT_TYPES.put(READABLE_ARRAY_TYPE, "Array"); + DEFAULT_TYPES.put(READABLE_MAP_TYPE, "Map"); + + BOXED_PRIMITIVES = new HashSet<>(); + BOXED_PRIMITIVES.add(TypeName.BOOLEAN.box()); + BOXED_PRIMITIVES.add(TypeName.FLOAT.box()); + BOXED_PRIMITIVES.add(TypeName.INT.box()); + } + + public ReactPropertyProcessor() { + mClasses = new HashMap<>(); + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + + mFiler = processingEnv.getFiler(); + mMessager = processingEnv.getMessager(); + mElements = processingEnv.getElementUtils(); + mTypes = processingEnv.getTypeUtils(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + // Clear properties from previous rounds + mClasses.clear(); + + Set elements = roundEnv.getElementsAnnotatedWith(ReactPropertyHolder.class); + for (Element element : elements) { + try { + TypeElement classType = (TypeElement) element; + ClassName className = ClassName.get(classType); + mClasses.put(className, parseClass(className, classType)); + } catch (Exception e) { + error(element, e.getMessage()); + } + } + + for (ClassInfo classInfo : mClasses.values()) { + try { + if (!shouldIgnoreClass(classInfo)) { + generateCode(classInfo, classInfo.mProperties); + } else if (shouldWarnClass(classInfo)) { + warning(classInfo.mElement, "Class was skipped. Classes need to be non-private."); + } + } catch (IOException e) { + error(e.getMessage()); + } catch (ReactPropertyException e) { + error(e.element, e.getMessage()); + } catch (Exception e) { + error(classInfo.mElement, e.getMessage()); + } + } + + return true; + } + + private ClassInfo parseClass(ClassName className, TypeElement typeElement) { + TypeName targetType = getTargetType(typeElement.asType()); + TypeName viewType = targetType.equals(SHADOW_NODE_TYPE) ? null : targetType; + + ClassInfo classInfo = new ClassInfo(className, typeElement, viewType); + + PropertyInfo.Builder propertyBuilder = new PropertyInfo.Builder(mTypes, mElements, classInfo); + for (Element element : mElements.getAllMembers(typeElement)) { + ReactProp prop = element.getAnnotation(ReactProp.class); + ReactPropGroup propGroup = element.getAnnotation(ReactPropGroup.class); + + try { + if (prop != null || propGroup != null) { + checkElement(element); + } + + if (prop != null) { + 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))); + } + } + } catch (ReactPropertyException e) { + error(e.element, e.getMessage()); + } + } + return classInfo; + } + + private TypeName getTargetType(TypeMirror mirror) { + TypeName typeName = TypeName.get(mirror); + if (typeName instanceof ParameterizedTypeName) { + ParameterizedTypeName parameterizedTypeName = (ParameterizedTypeName) typeName; + if (parameterizedTypeName.rawType.equals(VIEW_MANAGER_TYPE)) { + return parameterizedTypeName.typeArguments.get(0); + } + } else if (typeName.equals(SHADOW_NODE_TYPE)) { + return SHADOW_NODE_TYPE; + } else if (typeName.equals(TypeName.OBJECT)) { + throw new IllegalArgumentException("Could not find target type"); + } + + List types = mTypes.directSupertypes(mirror); + return getTargetType(types.get(0)); + } + + private void generateCode(ClassInfo classInfo, List properties) + throws IOException, ReactPropertyException { + MethodSpec getMethods = MethodSpec.methodBuilder("getProperties") + .addModifiers(PUBLIC) + .addAnnotation(Override.class) + .returns(PROPERTY_MAP_TYPE) + .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) + .addField(MAPPINGS_MAP_TYPE, "mappings", PRIVATE, STATIC, FINAL) + .addStaticBlock(generatePropertyMappings(properties)) + .addMethod(generateSetPropertySpec(classInfo, properties)) + .addMethod(getMethods) + .build(); + + JavaFile javaFile = JavaFile.builder(className.packageName(), holderClass) + .addFileComment("Generated by " + getClass().getName()) + .build(); + + javaFile.writeTo(mFiler); + } + + private String getClassName(TypeElement type, String packageName) { + int packageLen = packageName.length() + 1; + return type.getQualifiedName().toString().substring(packageLen).replace('.', '$'); + } + + private static TypeName getSuperType(ClassInfo classInfo) { + switch (classInfo.getType()) { + case VIEW_MANAGER: + return ParameterizedTypeName.get( + VIEW_MANAGER_SETTER_TYPE, + classInfo.mClassName, + classInfo.mViewType); + case SHADOW_NODE: + return ParameterizedTypeName.get(SHADOW_NODE_SETTER_TYPE, classInfo.mClassName); + default: + throw new IllegalArgumentException(); + } + } + + private static CodeBlock generatePropertyMappings(List properties) { + if (properties.isEmpty()) { + return CodeBlock.builder() + .addStatement("mappings = $T.emptyMap()", Collections.class) + .build(); + } + + CodeBlock.Builder builder = CodeBlock.builder() + .addStatement("mappings = new $T($L)", CONCRETE_MAPPINGS_MAP_TYPE, properties.size()); + + for (int i = 0, size = properties.size(); i < size; i++) { + PropertyInfo propertyInfo = properties.get(i); + builder.addStatement("mappings.put($S, $L)", propertyInfo.mProperty.name(), i); + } + + return builder.build(); + } + + private static MethodSpec generateSetPropertySpec( + ClassInfo classInfo, + List properties) { + MethodSpec.Builder builder = MethodSpec.methodBuilder("setProperty") + .addModifiers(PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.VOID); + + switch (classInfo.getType()) { + case VIEW_MANAGER: + builder + .addParameter(classInfo.mClassName, "manager") + .addParameter(classInfo.mViewType, "view"); + break; + case SHADOW_NODE: + builder + .addParameter(classInfo.mClassName, "node"); + break; + } + + return builder + .addParameter(STRING_TYPE, "name") + .addParameter(PROPS_TYPE, "props") + .addCode(generateSetProperty(classInfo, properties)) + .build(); + } + + private static CodeBlock generateSetProperty(ClassInfo info, List properties) { + if (properties.isEmpty()) { + return CodeBlock.builder().build(); + } + + CodeBlock.Builder builder = CodeBlock.builder() + .addStatement("Integer id = mappings.get(name)") + .addStatement("if (id == null) return"); + + builder.add("switch (id) {\n").indent(); + for (int i = 0, size = properties.size(); i < size; i++) { + PropertyInfo propertyInfo = properties.get(i); + builder + .add("case $L:\n", i) + .indent(); + + switch (info.getType()) { + case VIEW_MANAGER: + builder.add("manager.$L(view, ", propertyInfo.methodName); + break; + case SHADOW_NODE: + builder.add("node.$L(", propertyInfo.methodName); + break; + } + if (propertyInfo.mProperty instanceof GroupProperty) { + builder.add("$L, ", ((GroupProperty) propertyInfo.mProperty).mGroupIndex); + } + if (BOXED_PRIMITIVES.contains(propertyInfo.propertyType)) { + builder.add("props.isNull(name) ? null : "); + } + getPropertyExtractor(propertyInfo, builder); + builder.addStatement(")"); + + builder + .addStatement("break") + .unindent(); + } + builder.unindent().add("}\n"); + + return builder.build(); + } + + private static CodeBlock.Builder getPropertyExtractor( + PropertyInfo info, + CodeBlock.Builder builder) { + TypeName propertyType = info.propertyType; + if (propertyType.equals(STRING_TYPE)) { + return builder.add("props.getString(name)"); + } else if (propertyType.equals(READABLE_ARRAY_TYPE)) { + return builder.add("props.getArray(name)"); + } else if (propertyType.equals(READABLE_MAP_TYPE)) { + return builder.add("props.getMap(name)"); + } + + if (BOXED_PRIMITIVES.contains(propertyType)) { + propertyType = propertyType.unbox(); + } + + if (propertyType.equals(TypeName.BOOLEAN)) { + return builder.add("props.getBoolean(name, $L)", 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); + } else { + return builder.add("props.getDouble(name, $Lf)", 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); + } else { + return builder.add("props.getFloat(name, $Lf)", defaultFloat); + } + } + if (propertyType.equals(TypeName.INT)) { + return builder.add("props.getInt(name, $L)", info.mProperty.defaultInt()); + } + + throw new IllegalArgumentException(); + } + + private static CodeBlock generateGetProperties(List properties) + throws ReactPropertyException { + if (properties.isEmpty()) { + return CodeBlock.builder() + .addStatement("return $T.emptyMap()", Collections.class) + .build(); + } + + CodeBlock.Builder builder = CodeBlock.builder() + .addStatement( + "$T props = new $T($L)", + PROPERTY_MAP_TYPE, + CONCRETE_PROPERTY_MAP_TYPE, + properties.size()); + + for (PropertyInfo propertyInfo : properties) { + try { + String typeName = getPropertypTypeName(propertyInfo.mProperty, propertyInfo.propertyType); + builder.addStatement("props.put($S, $S)", propertyInfo.mProperty.name(), typeName); + } catch (IllegalArgumentException e) { + throw new ReactPropertyException(e.getMessage(), propertyInfo); + } + } + + return builder + .addStatement("return props") + .build(); + } + + 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; + 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)) { + return; + } + + throw new ReactPropertyException( + "@ReactProp and @ReachPropGroup annotation must be on a public method", + element); + } + + private static boolean shouldIgnoreClass(ClassInfo classInfo) { + return classInfo.mElement.getModifiers().contains(PRIVATE) + || classInfo.mElement.getModifiers().contains(ABSTRACT) + || classInfo.mViewType instanceof TypeVariableName; + } + + private static boolean shouldWarnClass(ClassInfo classInfo) { + return classInfo.mElement.getModifiers().contains(PRIVATE); + } + + private void error(Element element, String message) { + mMessager.printMessage(ERROR, message, element); + } + + private void error(String message) { + mMessager.printMessage(ERROR, message); + } + + private void warning(Element element, String message) { + mMessager.printMessage(WARNING, message, element); + } + + private interface Property { + String name(); + String customType(); + double defaultDouble(); + float defaultFloat(); + int defaultInt(); + boolean defaultBoolean(); + } + + private static class RegularProperty implements Property { + private final ReactProp mProp; + + public RegularProperty(ReactProp prop) { + mProp = prop; + } + + @Override + public String name() { + return mProp.name(); + } + + @Override + public String customType() { + return mProp.customType(); + } + + @Override + public double defaultDouble() { + return mProp.defaultDouble(); + } + + @Override + public float defaultFloat() { + return mProp.defaultFloat(); + } + + @Override + public int defaultInt() { + return mProp.defaultInt(); + } + + @Override + public boolean defaultBoolean() { + return mProp.defaultBoolean(); + } + } + + private static class GroupProperty implements Property { + private final ReactPropGroup mProp; + private final int mGroupIndex; + + public GroupProperty(ReactPropGroup prop, int groupIndex) { + mProp = prop; + mGroupIndex = groupIndex; + } + + @Override + public String name() { + return mProp.names()[mGroupIndex]; + } + + @Override + public String customType() { + return mProp.customType(); + } + + @Override + public double defaultDouble() { + return mProp.defaultDouble(); + } + + @Override + public float defaultFloat() { + return mProp.defaultFloat(); + } + + @Override + public int defaultInt() { + return mProp.defaultInt(); + } + + @Override + public boolean defaultBoolean() { + throw new UnsupportedOperationException(); + } + } + + private enum SettableType { + VIEW_MANAGER, + SHADOW_NODE + } + + private static class ClassInfo { + public final ClassName mClassName; + public final Element mElement; + public final @Nullable TypeName mViewType; + public final List mProperties; + + public ClassInfo(ClassName className, TypeElement element, @Nullable TypeName viewType) { + mClassName = className; + mElement = element; + mViewType = viewType; + mProperties = new ArrayList<>(); + } + + public SettableType getType() { + return mViewType == null ? SettableType.SHADOW_NODE : SettableType.VIEW_MANAGER; + } + + public void addProperty(PropertyInfo propertyInfo) throws ReactPropertyException { + String name = propertyInfo.mProperty.name(); + if (checkPropertyExists(name)) { + throw new ReactPropertyException( + "Module " + mClassName + " has already registered a property named \"" + + name + '"', propertyInfo); + } + + mProperties.add(propertyInfo); + } + + private boolean checkPropertyExists(String name) { + for (PropertyInfo propertyInfo : mProperties) { + if (propertyInfo.mProperty.name().equals(name)) { + return true; + } + } + + return false; + } + } + + private static class PropertyInfo { + public final String methodName; + public final TypeName propertyType; + public final Element element; + public final Property mProperty; + + private PropertyInfo( + String methodName, + TypeName propertyType, + Element element, + Property property) { + this.methodName = methodName; + this.propertyType = propertyType; + this.element = element; + mProperty = property; + } + + public static class Builder { + private final Types mTypes; + private final Elements mElements; + private final ClassInfo mClassInfo; + + public Builder(Types types, Elements elements, ClassInfo classInfo) { + mTypes = types; + mElements = elements; + mClassInfo = classInfo; + } + + public PropertyInfo build(Element element, Property property) + throws ReactPropertyException { + String methodName = element.getSimpleName().toString(); + + ExecutableElement method = (ExecutableElement) element; + List parameters = method.getParameters(); + + if (parameters.size() != getArgCount(mClassInfo.getType(), property)) { + throw new ReactPropertyException("Wrong number of args", element); + } + + int index = 0; + if (mClassInfo.getType() == SettableType.VIEW_MANAGER) { + TypeMirror mirror = parameters.get(index++).asType(); + if (!mTypes.isSubtype(mirror, mElements.getTypeElement("android.view.View").asType())) { + throw new ReactPropertyException("First argument must be a subclass of View", element); + } + } + + if (property instanceof GroupProperty) { + 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); + } + } + + 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); + } + + return new PropertyInfo(methodName, propertyType, element, property); + } + + private static int getArgCount(SettableType type, Property property) { + int baseCount = type == SettableType.SHADOW_NODE ? 1 : 2; + return property instanceof GroupProperty ? baseCount + 1 : baseCount; + } + } + } + + private static class ReactPropertyException extends Exception { + public final Element element; + + public ReactPropertyException(String message, PropertyInfo propertyInfo) { + super(message); + this.element = propertyInfo.element; + } + + public ReactPropertyException(String message, Element element) { + super(message); + this.element = element; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java index 21a88835d..de9ca3cf6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java @@ -12,12 +12,10 @@ package com.facebook.react.uimanager; import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Map; import com.facebook.csslayout.CSSNode; import com.facebook.infer.annotation.Assertions; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.uimanager.annotations.ReactPropertyHolder; /** * Base node class for representing virtual tree of React nodes. Shadow nodes are used primarily @@ -42,6 +40,7 @@ import com.facebook.react.bridge.ReadableMapKeySetIterator; * children (e.g. {@link #getNativeChildCount()}). See {@link NativeViewHierarchyOptimizer} for more * information. */ +@ReactPropertyHolder public class ReactShadowNode extends CSSNode { private int mReactTag; @@ -170,17 +169,7 @@ public class ReactShadowNode extends CSSNode { } public final void updateProperties(CatalystStylesDiffMap props) { - Map propSetters = - ViewManagersPropertyCache.getNativePropSettersForShadowNodeClass(getClass()); - ReadableMap propMap = props.mBackingMap; - ReadableMapKeySetIterator iterator = propMap.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); - if (setter != null) { - setter.updateShadowNodeProp(this, props); - } - } + ViewManagerPropertyUpdater.updateProps(this, props); onAfterUpdateTransaction(); } 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 ab14daa10..ca9fd66d3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java @@ -16,32 +16,22 @@ import java.util.Map; import android.view.View; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.touch.CatalystInterceptingViewGroup; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; +import com.facebook.react.uimanager.annotations.ReactPropertyHolder; /** * Class responsible for knowing how to create and update catalyst Views of a given type. It is also * responsible for creating and updating CSSNode subclasses used for calculating position and size * for the corresponding native view. */ +@ReactPropertyHolder public abstract class ViewManager { public final void updateProperties(T viewToUpdate, CatalystStylesDiffMap props) { - Map propSetters = - ViewManagersPropertyCache.getNativePropSettersForViewManagerClass(getClass()); - ReadableMap propMap = props.mBackingMap; - ReadableMapKeySetIterator iterator = propMap.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - ViewManagersPropertyCache.PropSetter setter = propSetters.get(key); - if (setter != null) { - setter.updateViewProp(this, viewToUpdate, props); - } - } + ViewManagerPropertyUpdater.updateProps(this, viewToUpdate, props); onAfterUpdateTransaction(viewToUpdate); } @@ -206,6 +196,6 @@ public abstract class ViewManager { } public Map getNativeProps() { - return ViewManagersPropertyCache.getNativePropsForView(getClass(), getShadowNodeClass()); + return ViewManagerPropertyUpdater.getNativeProps(getClass(), getShadowNodeClass()); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java new file mode 100644 index 000000000..9c8bdc5e5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagerPropertyUpdater.java @@ -0,0 +1,163 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager; + +import java.util.HashMap; +import java.util.Map; + +import android.view.View; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; + +public class ViewManagerPropertyUpdater { + public interface Settable { + Map getProperties(); + } + + public interface ViewManagerSetter extends Settable { + void setProperty(T manager, V view, String name, CatalystStylesDiffMap props); + } + + public interface ShadowNodeSetter extends Settable { + void setProperty(T node, String name, CatalystStylesDiffMap props); + } + + private static final String TAG = "ViewManagerPropertyUpdater"; + + private static final Map, ViewManagerSetter> VIEW_MANAGER_SETTER_MAP = + new HashMap<>(); + private static final Map, ShadowNodeSetter> SHADOW_NODE_SETTER_MAP = new HashMap<>(); + + public static void updateProps( + T manager, + V v, + CatalystStylesDiffMap 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); + } + } + + public static void updateProps(T node, CatalystStylesDiffMap 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); + } + } + + public static Map getNativeProps( + Class viewManagerTopClass, + Class shadowNodeTopClass) { + Map props = new HashMap<>(); + props.putAll(findManagerSetter(viewManagerTopClass).getProperties()); + props.putAll(findNodeSetter(shadowNodeTopClass).getProperties()); + return props; + } + + private static ViewManagerSetter findManagerSetter( + Class managerClass) { + @SuppressWarnings("unchecked") + ViewManagerSetter setter = + (ViewManagerSetter) VIEW_MANAGER_SETTER_MAP.get(managerClass); + if (setter == null) { + setter = findGeneratedSetter(managerClass); + if (setter == null) { + setter = new FallbackViewManagerSetter<>(managerClass); + } + VIEW_MANAGER_SETTER_MAP.put(managerClass, setter); + } + + return setter; + } + + private static ShadowNodeSetter findNodeSetter( + Class nodeClass) { + @SuppressWarnings("unchecked") + ShadowNodeSetter setter = (ShadowNodeSetter) SHADOW_NODE_SETTER_MAP.get(nodeClass); + if (setter == null) { + setter = findGeneratedSetter(nodeClass); + if (setter == null) { + setter = new FallbackShadowNodeSetter<>(nodeClass); + } + SHADOW_NODE_SETTER_MAP.put(nodeClass, setter); + } + + return setter; + } + + private static T findGeneratedSetter(Class cls) { + String clsName = cls.getName(); + try { + Class setterClass = Class.forName(clsName + "$$PropsSetter"); + //noinspection unchecked + return (T) setterClass.newInstance(); + } catch (ClassNotFoundException e) { + FLog.w(TAG, "Could not find generated setter for " + cls); + return null; + } catch (InstantiationException | IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate methods getter for " + clsName, e); + } + } + + private static class FallbackViewManagerSetter + implements ViewManagerSetter { + private final Map mPropSetters; + + private FallbackViewManagerSetter(Class viewManagerClass) { + mPropSetters = + ViewManagersPropertyCache.getNativePropSettersForViewManagerClass(viewManagerClass); + } + + @Override + public void setProperty(T manager, V v, String name, CatalystStylesDiffMap props) { + ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name); + if (setter != null) { + setter.updateViewProp(manager, v, props); + } + } + + @Override + public Map getProperties() { + Map nativeProps = new HashMap<>(); + for (ViewManagersPropertyCache.PropSetter setter : mPropSetters.values()) { + nativeProps.put(setter.getPropName(), setter.getPropType()); + } + return nativeProps; + } + } + + private static class FallbackShadowNodeSetter + implements ShadowNodeSetter { + private final Map mPropSetters; + + private FallbackShadowNodeSetter(Class shadowNodeClass) { + mPropSetters = + ViewManagersPropertyCache.getNativePropSettersForShadowNodeClass(shadowNodeClass); + } + + @Override + public void setProperty(ReactShadowNode node, String name, CatalystStylesDiffMap props) { + ViewManagersPropertyCache.PropSetter setter = mPropSetters.get(name); + if (setter != null) { + setter.updateShadowNodeProp(node, props); + } + } + + @Override + public Map getProperties() { + Map nativeProps = new HashMap<>(); + for (ViewManagersPropertyCache.PropSetter setter : mPropSetters.values()) { + nativeProps.put(setter.getPropName(), setter.getPropType()); + } + return nativeProps; + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/annotations/ReactPropertyHolder.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/annotations/ReactPropertyHolder.java new file mode 100644 index 000000000..f676e7e0c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/annotations/ReactPropertyHolder.java @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +package com.facebook.react.uimanager.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface ReactPropertyHolder { +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java index a47b262cc..14d9fc0d6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java @@ -32,7 +32,7 @@ public class ReactSwitchManager extends SimpleViewManager { private static final String REACT_CLASS = "AndroidSwitch"; - private static class ReactSwitchShadowNode extends LayoutShadowNode implements + static class ReactSwitchShadowNode extends LayoutShadowNode implements CSSNode.MeasureFunction { private int mWidth; 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 1c271fb31..0f3973bd2 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 @@ -84,14 +84,6 @@ public class ReactViewManager extends ViewGroupManager { null : ReactDrawableHelper.createDrawableFromJSDescription(view.getContext(), bg)); } - @ReactProp(name = ViewProps.BORDER_WIDTH, defaultFloat = CSSConstants.UNDEFINED) - public void setBorderWidth(ReactViewGroup view, float width) { - if (!CSSConstants.isUndefined(width)) { - width = PixelUtil.toPixelFromDIP(width); - } - view.setBorderWidth(Spacing.ALL, width); - } - @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS) public void setRemoveClippedSubviews(ReactViewGroup view, boolean removeClippedSubviews) { view.setRemoveClippedSubviews(removeClippedSubviews);