diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index d109ddb32ab..a72517110e2 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -662,12 +662,12 @@ public class com/facebook/react/bridge/CatalystInstanceImpl$PendingJSCall { public fun toString ()Ljava/lang/String; } -public class com/facebook/react/bridge/ColorPropConverter { - public fun ()V - public static fun getColor (Ljava/lang/Object;Landroid/content/Context;)Ljava/lang/Integer; - public static fun getColor (Ljava/lang/Object;Landroid/content/Context;I)Ljava/lang/Integer; - public static fun getColorInstance (Ljava/lang/Object;Landroid/content/Context;)Landroid/graphics/Color; - public static fun resolveResourcePath (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/Integer; +public final class com/facebook/react/bridge/ColorPropConverter { + public static final field INSTANCE Lcom/facebook/react/bridge/ColorPropConverter; + public static final fun getColor (Ljava/lang/Object;Landroid/content/Context;)Ljava/lang/Integer; + public static final fun getColor (Ljava/lang/Object;Landroid/content/Context;I)I + public static final fun getColorInstance (Ljava/lang/Object;Landroid/content/Context;)Landroid/graphics/Color; + public static final fun resolveResourcePath (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/Integer; } public final class com/facebook/react/bridge/CxxCallbackImpl : com/facebook/jni/HybridClassBase, com/facebook/react/bridge/Callback { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt index d24ad6bfa5a..bf41ba7d3b9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/ColorAnimatedNode.kt @@ -72,7 +72,7 @@ internal class ColorAnimatedNode( return } val context = context ?: return - val color = ColorPropConverter.getColor(nativeColor, context) + val color = ColorPropConverter.getColor(nativeColor, context) ?: return val rNode = nativeAnimatedNodesManager.getNodeById(rNodeId) as ValueAnimatedNode? val gNode = nativeAnimatedNodesManager.getNodeById(gNodeId) as ValueAnimatedNode? val bNode = nativeAnimatedNodesManager.getNodeById(bNodeId) as ValueAnimatedNode? diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java deleted file mode 100644 index 37504a9c64e..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and 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 android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.ColorSpace; -import android.os.Build; -import android.util.TypedValue; -import androidx.annotation.ColorLong; -import androidx.annotation.Nullable; -import androidx.core.content.res.ResourcesCompat; -import com.facebook.common.logging.FLog; -import com.facebook.react.common.ReactConstants; - -public class ColorPropConverter { - - private static boolean supportWideGamut() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - } - - private static final String JSON_KEY = "resource_paths"; - private static final String PREFIX_RESOURCE = "@"; - private static final String PREFIX_ATTR = "?"; - private static final String PACKAGE_DELIMITER = ":"; - private static final String PATH_DELIMITER = "/"; - private static final String ATTR = "attr"; - private static final String ATTR_SEGMENT = "attr/"; - - @Nullable - private static Integer getColorInteger(@Nullable Object value, Context context) { - if (value == null) { - return null; - } - - if (value instanceof Double) { - return ((Double) value).intValue(); - } - - if (context == null) { - throw new RuntimeException("Context may not be null."); - } - - if (value instanceof ReadableMap) { - ReadableMap map = (ReadableMap) value; - - if (map.hasKey("space")) { - int r = (int) ((float) map.getDouble("r") * 255); - int g = (int) ((float) map.getDouble("g") * 255); - int b = (int) ((float) map.getDouble("b") * 255); - int a = (int) ((float) map.getDouble("a") * 255); - - return Color.argb(a, r, g, b); - } - - ReadableArray resourcePaths = map.getArray(JSON_KEY); - - if (resourcePaths == null) { - throw new JSApplicationCausedNativeException( - "ColorValue: The `" + JSON_KEY + "` must be an array of color resource path strings."); - } - - for (int i = 0; i < resourcePaths.size(); i++) { - Integer result = resolveResourcePath(context, resourcePaths.getString(i)); - if (result != null) { - return result; - } - } - - throw new JSApplicationCausedNativeException( - "ColorValue: None of the paths in the `" - + JSON_KEY - + "` array resolved to a color resource."); - } - - throw new JSApplicationCausedNativeException( - "ColorValue: the value must be a number or Object."); - } - - @Nullable - public static Color getColorInstance(@Nullable Object value, Context context) { - if (value == null) { - return null; - } - - if (supportWideGamut() && value instanceof Double) { - return Color.valueOf(((Double) value).intValue()); - } - - if (context == null) { - throw new RuntimeException("Context may not be null."); - } - - if (value instanceof ReadableMap) { - ReadableMap map = (ReadableMap) value; - - if (supportWideGamut() && map.hasKey("space")) { - String rawColorSpace = map.getString("space"); - boolean isDisplayP3 = rawColorSpace != null && rawColorSpace.equals("display-p3"); - ColorSpace space = - ColorSpace.get(isDisplayP3 ? ColorSpace.Named.DISPLAY_P3 : ColorSpace.Named.SRGB); - float r = (float) map.getDouble("r"); - float g = (float) map.getDouble("g"); - float b = (float) map.getDouble("b"); - float a = (float) map.getDouble("a"); - - @ColorLong long color = Color.pack(r, g, b, a, space); - return Color.valueOf(color); - } - - ReadableArray resourcePaths = map.getArray(JSON_KEY); - if (resourcePaths == null) { - throw new JSApplicationCausedNativeException( - "ColorValue: The `" + JSON_KEY + "` must be an array of color resource path strings."); - } - - for (int i = 0; i < resourcePaths.size(); i++) { - Integer result = resolveResourcePath(context, resourcePaths.getString(i)); - if (supportWideGamut() && result != null) { - return Color.valueOf(result); - } - } - - throw new JSApplicationCausedNativeException( - "ColorValue: None of the paths in the `" - + JSON_KEY - + "` array resolved to a color resource."); - } - throw new JSApplicationCausedNativeException( - "ColorValue: the value must be a number or Object."); - } - - public static Integer getColor(@Nullable Object value, Context context) { - try { - if (supportWideGamut()) { - Color color = getColorInstance(value, context); - if (color != null) { - return color.toArgb(); - } - } - } catch (JSApplicationCausedNativeException ex) { - FLog.w(ReactConstants.TAG, ex, "Error extracting color from WideGamut"); - } - return getColorInteger(value, context); - } - - public static Integer getColor(@Nullable Object value, Context context, int defaultInt) { - try { - return getColor(value, context); - } catch (JSApplicationCausedNativeException e) { - FLog.w(ReactConstants.TAG, e, "Error converting ColorValue"); - return defaultInt; - } - } - - public static Integer resolveResourcePath(Context context, @Nullable String resourcePath) { - if (resourcePath == null || resourcePath.isEmpty()) { - return null; - } - - boolean isResource = resourcePath.startsWith(PREFIX_RESOURCE); - boolean isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR); - - resourcePath = resourcePath.substring(1); - - try { - if (isResource) { - return resolveResource(context, resourcePath); - } else if (isThemeAttribute) { - return resolveThemeAttribute(context, resourcePath); - } - } catch (Resources.NotFoundException exception) { - // The resource could not be found so do nothing to allow the for loop to continue and - // try the next fallback resource in the array. If none of the fallbacks are - // found then the exception immediately after the for loop will be thrown. - } - return null; - } - - private static int resolveResource(Context context, String resourcePath) { - String[] pathTokens = resourcePath.split(PACKAGE_DELIMITER); - - String packageName = context.getPackageName(); - String resource = resourcePath; - - if (pathTokens.length > 1) { - packageName = pathTokens[0]; - resource = pathTokens[1]; - } - - String[] resourceTokens = resource.split(PATH_DELIMITER); - String resourceType = resourceTokens[0]; - String resourceName = resourceTokens[1]; - - int resourceId = context.getResources().getIdentifier(resourceName, resourceType, packageName); - - return ResourcesCompat.getColor(context.getResources(), resourceId, context.getTheme()); - } - - private static int resolveThemeAttribute(Context context, String resourcePath) { - String path = resourcePath.replaceAll(ATTR_SEGMENT, ""); - String[] pathTokens = path.split(PACKAGE_DELIMITER); - - String packageName = context.getPackageName(); - String resourceName = path; - - if (pathTokens.length > 1) { - packageName = pathTokens[0]; - resourceName = pathTokens[1]; - } - - int resourceId = context.getResources().getIdentifier(resourceName, ATTR, packageName); - - // If resourceId is 0, try resolving with the android package name - if (resourceId == 0) { - resourceId = context.getResources().getIdentifier(resourceName, ATTR, "android"); - } - - TypedValue outValue = new TypedValue(); - Resources.Theme theme = context.getTheme(); - - if (theme.resolveAttribute(resourceId, outValue, true)) { - return outValue.data; - } - - throw new Resources.NotFoundException(); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.kt new file mode 100644 index 00000000000..fdff175db97 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ColorPropConverter.kt @@ -0,0 +1,214 @@ +/* + * Copyright (c) Meta Platforms, Inc. and 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 android.content.Context +import android.content.res.Resources +import android.graphics.Color +import android.graphics.ColorSpace +import android.os.Build +import android.util.TypedValue +import androidx.annotation.ColorLong +import androidx.core.content.res.ResourcesCompat +import com.facebook.common.logging.FLog +import com.facebook.react.common.ReactConstants + +public object ColorPropConverter { + + private fun supportWideGamut(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + + private const val JSON_KEY = "resource_paths" + private const val PREFIX_RESOURCE = "@" + private const val PREFIX_ATTR = "?" + private const val PACKAGE_DELIMITER = ":" + private const val PATH_DELIMITER = "/" + private const val ATTR = "attr" + private const val ATTR_SEGMENT = "attr/" + + private fun getColorInteger(value: Any?, context: Context): Int? { + if (value == null) { + return null + } + + if (value is Double) { + return value.toInt() + } + + checkNotNull(context) + + if (value is ReadableMap) { + if (value.hasKey("space")) { + val r = (value.getDouble("r").toFloat() * 255).toInt() + val g = (value.getDouble("g").toFloat() * 255).toInt() + val b = (value.getDouble("b").toFloat() * 255).toInt() + val a = (value.getDouble("a").toFloat() * 255).toInt() + + return Color.argb(a, r, g, b) + } + + val resourcePaths = + value.getArray(JSON_KEY) + ?: throw JSApplicationCausedNativeException( + "ColorValue: The `$JSON_KEY` must be an array of color resource path strings.") + + for (i in 0 until resourcePaths.size()) { + val result = resolveResourcePath(context, resourcePaths.getString(i)) + if (result != null) { + return result + } + } + + throw JSApplicationCausedNativeException( + "ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource.") + } + + throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.") + } + + @JvmStatic + public fun getColorInstance(value: Any?, context: Context): Color? { + if (value == null) { + return null + } + + if (supportWideGamut() && value is Double) { + return Color.valueOf(value.toInt()) + } + + checkNotNull(context) + + if (value is ReadableMap) { + if (supportWideGamut() && value.hasKey("space")) { + val rawColorSpace = value.getString("space") + val isDisplayP3 = rawColorSpace == "display-p3" + val space = + ColorSpace.get(if (isDisplayP3) ColorSpace.Named.DISPLAY_P3 else ColorSpace.Named.SRGB) + + val r = value.getDouble("r").toFloat() + val g = value.getDouble("g").toFloat() + val b = value.getDouble("b").toFloat() + val a = value.getDouble("a").toFloat() + + @ColorLong val color = Color.pack(r, g, b, a, space) + return Color.valueOf(color) + } + + val resourcePaths = + value.getArray(JSON_KEY) + ?: throw JSApplicationCausedNativeException( + "ColorValue: The `$JSON_KEY` must be an array of color resource path strings.") + + for (i in 0 until resourcePaths.size()) { + val result = resolveResourcePath(context, resourcePaths.getString(i)) + if (supportWideGamut() && result != null) { + return Color.valueOf(result) + } + } + + throw JSApplicationCausedNativeException( + "ColorValue: None of the paths in the `$JSON_KEY` array resolved to a color resource.") + } + + throw JSApplicationCausedNativeException("ColorValue: the value must be a number or Object.") + } + + @JvmStatic + public fun getColor(value: Any?, context: Context): Int? { + try { + if (supportWideGamut()) { + val color = getColorInstance(value, context) + if (color != null) { + return color.toArgb() + } + } + } catch (ex: JSApplicationCausedNativeException) { + FLog.w(ReactConstants.TAG, ex, "Error extracting color from WideGamut") + } + + return getColorInteger(value, context) + } + + @JvmStatic + public fun getColor(value: Any?, context: Context, defaultInt: Int): Int { + return try { + getColor(value, context) ?: defaultInt + } catch (e: JSApplicationCausedNativeException) { + FLog.w(ReactConstants.TAG, e, "Error converting ColorValue") + defaultInt + } + } + + @JvmStatic + public fun resolveResourcePath(context: Context, resourcePath: String?): Int? { + if (resourcePath.isNullOrEmpty()) { + return null + } + + val isResource = resourcePath.startsWith(PREFIX_RESOURCE) + val isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR) + + val path = resourcePath.substring(1) + + return try { + when { + isResource -> resolveResource(context, path) + isThemeAttribute -> resolveThemeAttribute(context, path) + else -> null + } + } catch (e: Resources.NotFoundException) { + null + } + } + + private fun resolveResource(context: Context, resourcePath: String): Int { + val pathTokens = resourcePath.split(PACKAGE_DELIMITER) + var packageName = context.packageName + var resource = resourcePath + + if (pathTokens.size > 1) { + packageName = pathTokens[0] + resource = pathTokens[1] + } + + val resourceTokens = resource.split(PATH_DELIMITER) + val resourceType = resourceTokens[0] + val resourceName = resourceTokens[1] + + val resourceId = context.resources.getIdentifier(resourceName, resourceType, packageName) + + return ResourcesCompat.getColor(context.resources, resourceId, context.theme) + } + + private fun resolveThemeAttribute(context: Context, resourcePath: String): Int { + val path = resourcePath.replace(ATTR_SEGMENT, "") + val pathTokens = path.split(PACKAGE_DELIMITER) + + var packageName = context.packageName + var resourceName = path + + if (pathTokens.size > 1) { + packageName = pathTokens[0] + resourceName = pathTokens[1] + } + + var resourceId = context.resources.getIdentifier(resourceName, ATTR, packageName) + + if (resourceId == 0) { + resourceId = context.resources.getIdentifier(resourceName, ATTR, "android") + } + + val outValue = TypedValue() + val theme = context.theme + + if (theme.resolveAttribute(resourceId, outValue, true)) { + return outValue.data + } + + throw Resources.NotFoundException() + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt index 01b609cf677..b2b16a0b99c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt @@ -46,8 +46,7 @@ public abstract class BaseViewManagerDelegate< mViewManager.setAccessibilityValue(view, value as ReadableMap?) ViewProps.BACKGROUND_COLOR -> - mViewManager.setBackgroundColor( - view, if (value == null) 0 else ColorPropConverter.getColor(value, view.context)) + mViewManager.setBackgroundColor(view, ColorPropConverter.getColor(value, view.context, 0)) ViewProps.BORDER_RADIUS -> mViewManager.setBorderRadius( @@ -78,8 +77,7 @@ public abstract class BaseViewManagerDelegate< ViewProps.MIX_BLEND_MODE -> mViewManager.setMixBlendMode(view, value as String?) ViewProps.SHADOW_COLOR -> - mViewManager.setShadowColor( - view, if (value == null) 0 else ColorPropConverter.getColor(value, view.context)) + mViewManager.setShadowColor(view, ColorPropConverter.getColor(value, view.context, 0)) ViewProps.IMPORTANT_FOR_ACCESSIBILITY -> mViewManager.setImportantForAccessibility(view, value as String?) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.kt index c52887b07d7..decf94f66f6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.kt @@ -52,7 +52,7 @@ internal open class SwipeRefreshLayoutManager : val colorValues = IntArray(colors.size()) for (i in 0..