Kotlinify ColorPropConverter (#50626)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50626

As per title.

Changelog:
[Android][Breaking] Kotlinify ColorPropConverter

Reviewed By: javache

Differential Revision: D72788490

fbshipit-source-id: 1c81c70566437db36e45e59995093c4c808369d8
This commit is contained in:
Fabrizio Cucci
2025-04-11 05:14:34 -07:00
committed by Facebook GitHub Bot
parent 398f2068d1
commit 57768bfbcd
6 changed files with 224 additions and 246 deletions
@@ -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 <init> ()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 {
@@ -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?
@@ -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();
}
}
@@ -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()
}
}
@@ -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?)
@@ -52,7 +52,7 @@ internal open class SwipeRefreshLayoutManager :
val colorValues = IntArray(colors.size())
for (i in 0..<colors.size()) {
if (colors.getType(i) == ReadableType.Map) {
colorValues[i] = ColorPropConverter.getColor(colors.getMap(i), view.context)
colorValues[i] = ColorPropConverter.getColor(colors.getMap(i), view.context, 0)
} else {
colorValues[i] = colors.getInt(i)
}