diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index 87ffae6ef54d01..fbda4b84d7a999 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -10,9 +10,11 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.Bundle; import android.view.KeyEvent; +import android.view.Window; import androidx.annotation.Nullable; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Callback; @@ -99,6 +101,9 @@ public String getMainComponentName() { protected void onCreate(Bundle savedInstanceState) { String mainComponentName = getMainComponentName(); final Bundle launchOptions = composeLaunchOptions(); + if (isWideColorGamutEnabled()) { + mActivity.getWindow().setColorMode(ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT); + } if (ReactFeatureFlags.enableBridgelessArchitecture) { mReactDelegate = new ReactDelegate(getPlainActivity(), getReactHost(), mainComponentName, launchOptions); @@ -239,4 +244,13 @@ protected Activity getPlainActivity() { protected boolean isFabricEnabled() { return ReactFeatureFlags.enableFabricRenderer; } + + /** + * Override this method if you wish to selectively toggle wide color gamut for a specific surface. + * + * @return true if wide gamut is enabled for this Activity, false otherwise. + */ + protected boolean isWideColorGamutEnabled() { + return false; + } } 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 index afc690dedaabbf..888c100daae6e6 100644 --- 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 @@ -9,7 +9,10 @@ import android.content.Context; import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.ColorSpace; import android.util.TypedValue; +import androidx.annotation.ColorLong; import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import com.facebook.common.logging.FLog; @@ -24,13 +27,13 @@ public class ColorPropConverter { private static final String ATTR = "attr"; private static final String ATTR_SEGMENT = "attr/"; - public static Integer getColor(Object value, Context context) { + public static Color getColorInstance(Object value, Context context) { if (value == null) { return null; } if (value instanceof Double) { - return ((Double) value).intValue(); + return Color.valueOf(((Double) value).intValue()); } if (context == null) { @@ -39,6 +42,22 @@ public static Integer getColor(Object value, Context context) { if (value instanceof ReadableMap) { ReadableMap map = (ReadableMap) value; + + // handle color(space r g b a) value + if (map.hasKey("space")) { + String rawColorSpace = map.getString("space"); + boolean isDisplayP3 = 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) { @@ -49,7 +68,7 @@ public static Integer getColor(Object value, Context context) { for (int i = 0; i < resourcePaths.size(); i++) { Integer result = resolveResourcePath(context, resourcePaths.getString(i)); if (result != null) { - return result; + return Color.valueOf(result); } } @@ -63,6 +82,14 @@ public static Integer getColor(Object value, Context context) { "ColorValue: the value must be a number or Object."); } + public static Integer getColor(Object value, Context context) { + Color color = getColorInstance(value, context); + if (color == null) { + return null; + } + return color.toArgb(); + } + public static Integer getColor(Object value, Context context, int defaultInt) { try { return getColor(value, context); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JSONArguments.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JSONArguments.java index 6e336ba1a88993..213908ce2a687e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JSONArguments.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JSONArguments.java @@ -39,7 +39,7 @@ public static ReadableMap fromJSONObject(JSONObject obj) throws JSONException { } else if (val instanceof Double) { result.putDouble(key, (Double) val); } else if (val instanceof Long) { - result.putInt(key, ((Long) val).intValue()); + result.putLong(key, (Long) val); } else if (obj.isNull(key)) { result.putNull(key); } else { @@ -86,7 +86,7 @@ public static ReadableArray fromJSONArray(JSONArray arr) throws JSONException { } else if (val instanceof Double) { result.pushDouble((Double) val); } else if (val instanceof Long) { - result.pushInt(((Long) val).intValue()); + result.pushLong((Long) val); } else if (arr.isNull(i)) { result.pushNull(); } else { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java index f81766a4c0243d..5a1ed1d7a92ec3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyArray.java @@ -96,6 +96,11 @@ public int getInt(int index) { return ((Number) mBackingList.get(index)).intValue(); } + @Override + public long getLong(int index) { + return ((Number) mBackingList.get(index)).longValue(); + } + @Override public @Nullable String getString(int index) { return (String) mBackingList.get(index); @@ -129,7 +134,7 @@ public ReadableMap getMap(int index) { return ReadableType.Null; } else if (object instanceof Boolean) { return ReadableType.Boolean; - } else if (object instanceof Double || object instanceof Float || object instanceof Integer) { + } else if (object instanceof Double || object instanceof Float || object instanceof Integer || object instanceof Long) { return ReadableType.Number; } else if (object instanceof String) { return ReadableType.String; @@ -156,6 +161,11 @@ public void pushInt(int value) { mBackingList.add(new Double(value)); } + @Override + public void pushLong(long value) { + mBackingList.add(new Double(value)); + } + @Override public void pushString(@Nullable String value) { mBackingList.add(value); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java index ccce30a7d60e36..e72bdfff421170 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/JavaOnlyMap.java @@ -110,6 +110,11 @@ public int getInt(@NonNull String name) { return ((Number) mBackingMap.get(name)).intValue(); } + @Override + public long getLong(@NonNull String name) { + return ((Number) mBackingMap.get(name)).longValue(); + } + @Override public String getString(@NonNull String name) { return (String) mBackingMap.get(name); @@ -190,6 +195,11 @@ public void putInt(@NonNull String key, int value) { mBackingMap.put(key, new Double(value)); } + @Override + public void putLong(@NonNull String key, long value) { + mBackingMap.put(key, new Double(value)); + } + @Override public void putString(@NonNull String key, @Nullable String value) { mBackingMap.put(key, value); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java index 26a88911b30cae..9a5e84aaeae848 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableArray.java @@ -26,6 +26,8 @@ public interface ReadableArray { int getInt(int index); + long getLong(int index); + @NonNull String getString(int index); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java index a3bae65f7224c4..3dcc141a8cc232 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableMap.java @@ -29,6 +29,8 @@ public interface ReadableMap { int getInt(@NonNull String name); + long getLong(@NonNull String name); + @Nullable String getString(@NonNull String name); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java index 1a117a6b46928c..19543b173b3f5d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeArray.java @@ -97,6 +97,11 @@ public int getInt(int index) { return ((Double) getLocalArray()[index]).intValue(); } + @Override + public long getLong(int index) { + return ((Double) getLocalArray()[index]).longValue(); + } + @Override public @NonNull String getString(int index) { return (String) getLocalArray()[index]; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java index 0ade85c10090f1..4519c8b33e0902 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReadableNativeMap.java @@ -160,6 +160,12 @@ public int getInt(@NonNull String name) { return getValue(name, Double.class).intValue(); } + @Override + public long getLong(@NonNull String name) { + // All numbers coming out of native are doubles, so cast here then truncate + return getValue(name, Double.class).longValue(); + } + @Override public @Nullable String getString(@NonNull String name) { return getNullableValue(name, String.class); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java index 8ec699c3970ed9..0b7cb17f69487c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableArray.java @@ -20,6 +20,8 @@ public interface WritableArray extends ReadableArray { void pushInt(int value); + void pushLong(long value); + void pushString(@Nullable String value); void pushArray(@Nullable ReadableArray array); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java index 1f6ec6500970f1..a7ef7818f51bb5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableMap.java @@ -21,6 +21,8 @@ public interface WritableMap extends ReadableMap { void putInt(@NonNull String key, int value); + void putLong(@NonNull String key, long value); + void putString(@NonNull String key, @Nullable String value); void putArray(@NonNull String key, @Nullable ReadableArray value); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java index 46a2c8d9e67e34..06ad5650e11c45 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeArray.java @@ -38,6 +38,9 @@ public WritableNativeArray() { @Override public native void pushInt(int value); + @Override + public native void pushLong(long value); + @Override public native void pushString(@Nullable String value); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java index 809cc9127f555e..228b3065de7a39 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/WritableNativeMap.java @@ -32,6 +32,9 @@ public class WritableNativeMap extends ReadableNativeMap implements WritableMap @Override public native void putInt(@NonNull String key, int value); + @Override + public native void putLong(@NonNull String key, long value); + @Override public native void putNull(@NonNull String key); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt index 4530302021c1dc..f5bb2bcad2b4dc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/MapBuffer.kt @@ -44,7 +44,8 @@ interface MapBuffer : Iterable { INT, DOUBLE, STRING, - MAP + MAP, + LONG } /** @@ -109,6 +110,16 @@ interface MapBuffer : Iterable { */ fun getInt(key: Int): Int + /** + * Provides parsed [Long] value if the entry for given key exists with [DataType.LONG] type + * + * @param key key to lookup [Long] value for + * @return value associated with the requested key + * @throws IllegalArgumentException if the key doesn't exist + * @throws IllegalStateException if the data type doesn't match + */ + fun getLong(key: Int): Long + /** * Provides parsed [Double] value if the entry for given key exists with [DataType.DOUBLE] type * @@ -175,6 +186,13 @@ interface MapBuffer : Iterable { */ val intValue: Int + /** + * Entry value represented as [Long] + * + * @throws IllegalStateException if the data type doesn't match [DataType.LONG] + */ + val longValue: Long + /** * Entry value represented as [Double] * diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt index f4ea0f33ec8473..fdca3ee8b73faa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt @@ -115,6 +115,10 @@ class ReadableMapBuffer : MapBuffer { return buffer.getInt(bufferPosition) } + private fun readLongValue(bufferPosition: Int): Long { + return buffer.getLong(bufferPosition) + } + private fun readBooleanValue(bufferPosition: Int): Boolean { return readIntValue(bufferPosition) == 1 } @@ -180,6 +184,9 @@ class ReadableMapBuffer : MapBuffer { override fun getInt(key: Int): Int = readIntValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.INT)) + override fun getLong(key: Int): Long = + readLongValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.LONG)) + override fun getDouble(key: Int): Double = readDoubleValue(getTypedValueOffsetForKey(key, MapBuffer.DataType.DOUBLE)) @@ -223,6 +230,7 @@ class ReadableMapBuffer : MapBuffer { when (entry.type) { MapBuffer.DataType.BOOL -> builder.append(entry.booleanValue) MapBuffer.DataType.INT -> builder.append(entry.intValue) + MapBuffer.DataType.LONG -> builder.append(entry.longValue) MapBuffer.DataType.DOUBLE -> builder.append(entry.doubleValue) MapBuffer.DataType.STRING -> builder.append(entry.stringValue) MapBuffer.DataType.MAP -> builder.append(entry.mapBufferValue.toString()) @@ -280,6 +288,12 @@ class ReadableMapBuffer : MapBuffer { return readIntValue(bucketOffset + VALUE_OFFSET) } + override val longValue: Long + get() { + assertType(MapBuffer.DataType.LONG) + return readLongValue(bucketOffset + VALUE_OFFSET) + } + override val booleanValue: Boolean get() { assertType(MapBuffer.DataType.BOOL) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt index b687f7a77bc975..ec835690178a87 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/WritableMapBuffer.kt @@ -48,6 +48,15 @@ class WritableMapBuffer : MapBuffer { */ fun put(key: Int, value: Int): WritableMapBuffer = putInternal(key, value) + /** + * Adds a long value for given key to the MapBuffer. + * + * @param key entry key + * @param value entry value + * @throws IllegalArgumentException if key is out of [UShort] range + */ + fun put(key: Int, value: Long): WritableMapBuffer = putInternal(key, value) + /** * Adds a double value for given key to the MapBuffer. * @@ -107,6 +116,8 @@ class WritableMapBuffer : MapBuffer { override fun getInt(key: Int): Int = verifyValue(key, values.get(key)) + override fun getLong(key: Int): Long = verifyValue(key, values.get(key)) + override fun getDouble(key: Int): Double = verifyValue(key, values.get(key)) override fun getString(key: Int): String = verifyValue(key, values.get(key)) @@ -128,6 +139,7 @@ class WritableMapBuffer : MapBuffer { return when (val value = this) { is Boolean -> DataType.BOOL is Int -> DataType.INT + is Long -> DataType.LONG is Double -> DataType.DOUBLE is String -> DataType.STRING is MapBuffer -> DataType.MAP @@ -153,6 +165,9 @@ class WritableMapBuffer : MapBuffer { override val intValue: Int get() = verifyValue(key, values.valueAt(index)) + override val longValue: Long + get() = verifyValue(key, values.valueAt(index)) + override val doubleValue: Double get() = verifyValue(key, values.valueAt(index)) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 3e3d86e87dcf0c..b55d5f9e21edbd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -30,6 +30,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.PointerEventHelper; import com.facebook.react.uimanager.util.ReactFindViewUtil; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -42,6 +43,7 @@ public abstract class BaseViewManager extends ViewManager implements BaseViewManagerInterface, View.OnLayoutChangeListener { + private static final String TAG = "BaseViewManager"; private static final int PERSPECTIVE_ARRAY_INVERTED_CAMERA_DISTANCE_INDEX = 2; private static final float CAMERA_DISTANCE_NORMALIZATION_MULTIPLIER = (float) Math.sqrt(5); @@ -164,14 +166,26 @@ public void onLayoutChange( } @Override - @ReactProp( - name = ViewProps.BACKGROUND_COLOR, - defaultInt = Color.TRANSPARENT, - customType = "Color") public void setBackgroundColor(@NonNull T view, int backgroundColor) { view.setBackgroundColor(backgroundColor); } + @ReactProp( + name = ViewProps.BACKGROUND_COLOR, + defaultInt = Color.TRANSPARENT, + customType = "Color") + public void setBackgroundColor(@NonNull T view, long backgroundColor) { + try { + Method setBackgroundColorMethod = view.getClass().getMethod("setBackgroundColor", long.class); + setBackgroundColorMethod.invoke(view, backgroundColor); + } catch (NoSuchMethodException e) { + FLog.e(TAG, "setBackgroundColor(long) method not found in view class", e); + view.setBackgroundColor(Color.toArgb(backgroundColor)); + } catch (Exception e) { + FLog.e(TAG, "Failed to invoke setBackgroundColor(long)", e); + } + } + @Override @ReactProp(name = ViewProps.TRANSFORM) public void setTransform(@NonNull T view, @Nullable ReadableArray matrix) { @@ -204,7 +218,6 @@ public void setElevation(@NonNull T view, float elevation) { } @Override - @ReactProp(name = ViewProps.SHADOW_COLOR, defaultInt = Color.BLACK, customType = "Color") public void setShadowColor(@NonNull T view, int shadowColor) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { view.setOutlineAmbientShadowColor(shadowColor); @@ -212,6 +225,14 @@ public void setShadowColor(@NonNull T view, int shadowColor) { } } + @ReactProp(name = ViewProps.SHADOW_COLOR, defaultInt = Color.BLACK, customType = "Color") + public void setShadowColor(@NonNull T view, long shadowColor) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + view.setOutlineAmbientShadowColor(Color.toArgb(shadowColor)); + view.setOutlineSpotShadowColor(Color.toArgb(shadowColor)); + } + } + @Override @ReactProp(name = ViewProps.Z_INDEX) public void setZIndex(@NonNull T view, float zIndex) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java index 6ef232813dbc3f..2f8208a4012e31 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerInterface.java @@ -35,6 +35,7 @@ public interface BaseViewManagerInterface { void setViewState(T view, @Nullable ReadableMap accessibilityState); void setBackgroundColor(T view, int backgroundColor); + void setBackgroundColor(T view, long backgroundColor); void setBorderRadius(T view, float borderRadius); @@ -49,6 +50,7 @@ public interface BaseViewManagerInterface { void setElevation(T view, float elevation); void setShadowColor(T view, int shadowColor); + void setShadowColor(T view, long shadowColor); void setImportantForAccessibility(T view, @Nullable String importantForAccessibility); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BorderColor.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BorderColor.java new file mode 100644 index 00000000000000..24fbf5d2c4bc76 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BorderColor.java @@ -0,0 +1,178 @@ +/* + * 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.uimanager; + + import android.graphics.Color; + import java.util.Arrays; + + /** + * Class representing CSS spacing border colors. Modified from {@link Spacing} to support long values. + */ + public class BorderColor { + + /** Spacing type that represents the left direction. E.g. {@code marginLeft}. */ + public static final int LEFT = 0; + /** Spacing type that represents the top direction. E.g. {@code marginTop}. */ + public static final int TOP = 1; + /** Spacing type that represents the right direction. E.g. {@code marginRight}. */ + public static final int RIGHT = 2; + /** Spacing type that represents the bottom direction. E.g. {@code marginBottom}. */ + public static final int BOTTOM = 3; + /** + * Spacing type that represents start direction e.g. left in left-to-right, right in + * right-to-left. + */ + public static final int START = 4; + /** + * Spacing type that represents end direction e.g. right in left-to-right, left in right-to-left. + */ + public static final int END = 5; + /** + * Spacing type that represents horizontal direction (left and right). E.g. {@code + * marginHorizontal}. + */ + public static final int HORIZONTAL = 6; + /** + * Spacing type that represents vertical direction (top and bottom). E.g. {@code marginVertical}. + */ + public static final int VERTICAL = 7; + /** + * Spacing type that represents all directions (left, top, right, bottom). E.g. {@code margin}. + */ + public static final int ALL = 8; + /** Spacing type that represents block directions (top, bottom). E.g. {@code marginBlock}. */ + public static final int BLOCK = 9; + /** Spacing type that represents the block end direction (bottom). E.g. {@code marginBlockEnd}. */ + public static final int BLOCK_END = 10; + /** + * Spacing type that represents the block start direction (top). E.g. {@code marginBlockStart}. + */ + public static final int BLOCK_START = 11; + + private static final int[] sFlagsMap = { + 1, /*LEFT*/ 2, /*TOP*/ 4, /*RIGHT*/ 8, /*BOTTOM*/ 16, /*START*/ 32, /*END*/ 64, /*HORIZONTAL*/ + 128, /*VERTICAL*/ 256, /*ALL*/ 512, /*BLOCK*/ 1024, /*BLOCK_END*/ 2048, /*BLOCK_START*/ + }; + + private final long[] mSpacing; + private int mValueFlags = 0; + private final long mDefaultValue; + private boolean mHasAliasesSet; + + public BorderColor() { + this(0); + } + + public BorderColor(long defaultValue) { + mDefaultValue = defaultValue; + mSpacing = newFullBorderColorArray(); + } + + public BorderColor(BorderColor original) { + mDefaultValue = original.mDefaultValue; + mSpacing = Arrays.copyOf(original.mSpacing, original.mSpacing.length); + mValueFlags = original.mValueFlags; + mHasAliasesSet = original.mHasAliasesSet; + } + + /** + * Set a spacing value. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, {@link + * #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + * @param value the value for this direction + * @return {@code true} if the spacing has changed, or {@code false} if the same value was already + * set + */ + public boolean set(int spacingType, long value) { + if (mSpacing[spacingType] != value) { + mSpacing[spacingType] = value; + + mValueFlags |= sFlagsMap[spacingType]; + + mHasAliasesSet = + (mValueFlags & sFlagsMap[ALL]) != 0 + || (mValueFlags & sFlagsMap[VERTICAL]) != 0 + || (mValueFlags & sFlagsMap[HORIZONTAL]) != 0 + || (mValueFlags & sFlagsMap[BLOCK]) != 0; + + return true; + } + + return false; + } + + /** + * Get the spacing for a direction. This takes into account any default values that have been set. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM} + */ + public long get(int spacingType) { + long defaultValue = + (spacingType == START + || spacingType == END + || spacingType == BLOCK + || spacingType == BLOCK_END + || spacingType == BLOCK_START + ? 0 + : mDefaultValue); + + if (mValueFlags == 0) { + return defaultValue; + } + + if ((mValueFlags & sFlagsMap[spacingType]) != 0) { + return mSpacing[spacingType]; + } + + if (mHasAliasesSet) { + int secondType = spacingType == TOP || spacingType == BOTTOM ? VERTICAL : HORIZONTAL; + if ((mValueFlags & sFlagsMap[secondType]) != 0) { + return mSpacing[secondType]; + } else if ((mValueFlags & sFlagsMap[ALL]) != 0) { + return mSpacing[ALL]; + } + } + + return defaultValue; + } + + /** + * Get the raw value (that was set using {@link #set(int, float)}), without taking into account + * any default values. + * + * @param spacingType one of {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, {@link #BOTTOM}, {@link + * #VERTICAL}, {@link #HORIZONTAL}, {@link #ALL} + */ + public long getRaw(int spacingType) { + return mSpacing[spacingType]; + } + + /** + * Resets the spacing instance to its default state. This method is meant to be used when + * recycling {@link Spacing} instances. + */ + public void reset() { + Arrays.fill(mSpacing, 0); + mHasAliasesSet = false; + mValueFlags = 0; + } + + /** + * Try to get start value and fallback to given type if not defined. This is used privately by the + * layout engine as a more efficient way to fetch direction-aware values by avoid extra method + * invocations. + */ + float getWithFallback(int spacingType, int fallbackType) { + return (mValueFlags & sFlagsMap[spacingType]) != 0 ? mSpacing[spacingType] : get(fallbackType); + } + + private static long[] newFullBorderColorArray() { + return new long[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + } + } \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMap.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMap.java index 504bedbfe220da..c1d730c88fe1ae 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMap.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactStylesDiffMap.java @@ -70,6 +70,10 @@ public int getInt(String name, int restoreNullToDefaultValue) { return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getInt(name); } + public long getLong(String name, long restoreNullToDefaultValue) { + return mBackingMap.isNull(name) ? restoreNullToDefaultValue : mBackingMap.getLong(name); + } + @Nullable public String getString(String name) { return mBackingMap.getString(name); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java index 09ae7a272ad0e6..d391ae6e51cbb1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.java @@ -6,8 +6,8 @@ */ package com.facebook.react.uimanager; - import android.content.Context; +import android.graphics.Color; import android.view.View; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -211,11 +211,12 @@ public ColorPropSetter(ReactPropGroup prop, Method setter, int index, int defaul @Override protected Object getValueOrDefault(Object value, Context context) { - if (value == null) { + Color color = ColorPropConverter.getColorInstance(value, context); + if (color == null) { return mDefaultValue; } - return ColorPropConverter.getColor(value, context); + return color.pack(); } } @@ -342,10 +343,11 @@ public BoxedColorPropSetter(ReactPropGroup prop, Method setter, int index) { @Override protected @Nullable Object getValueOrDefault(Object value, Context context) { - if (value != null) { - return ColorPropConverter.getColor(value, context); + Color color = ColorPropConverter.getColorInstance(value, context); + if (color == null) { + return null; } - return null; + return color.pack(); } } @@ -437,6 +439,8 @@ private static PropSetter createPropSetter( return new ColorPropSetter(annotation, method, annotation.defaultInt()); } return new IntPropSetter(annotation, method, annotation.defaultInt()); + } else if (propTypeClass == long.class && "Color".equals(annotation.customType())) { + return new ColorPropSetter(annotation, method, annotation.defaultInt()); } else if (propTypeClass == float.class) { return new FloatPropSetter(annotation, method, annotation.defaultFloat()); } else if (propTypeClass == double.class) { @@ -450,6 +454,8 @@ private static PropSetter createPropSetter( return new BoxedColorPropSetter(annotation, method); } return new BoxedIntPropSetter(annotation, method); + } else if (propTypeClass == Long.class && "Color".equals(annotation.customType())) { + return new BoxedColorPropSetter(annotation, method); } else if (propTypeClass == ReadableArray.class) { return new ArrayPropSetter(annotation, method); } else if (propTypeClass == ReadableMap.class) { @@ -483,6 +489,10 @@ private static void createPropSetters( props.put(names[i], new IntPropSetter(annotation, method, i, annotation.defaultInt())); } } + } else if (propTypeClass == long.class && "Color".equals(annotation.customType())) { + for (int i = 0; i < names.length; i++) { + props.put(names[i], new ColorPropSetter(annotation, method, i, annotation.defaultInt())); + } } else if (propTypeClass == float.class) { for (int i = 0; i < names.length; i++) { props.put(names[i], new FloatPropSetter(annotation, method, i, annotation.defaultFloat())); @@ -500,6 +510,10 @@ private static void createPropSetters( props.put(names[i], new BoxedIntPropSetter(annotation, method, i)); } } + } else if (propTypeClass == Long.class && "Color".equals(annotation.customType())) { + for (int i = 0; i < names.length; i++) { + props.put(names[i], new BoxedColorPropSetter(annotation, method, i)); + } } else { throw new RuntimeException( "Unrecognized type: " @@ -611,4 +625,4 @@ protected Object[] initialValue() { } }; } -} +} \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java index 7e24f0d7345574..ab75a8fdbdd159 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/drawer/ReactDrawerLayoutManager.java @@ -165,13 +165,17 @@ public void closeDrawer(ReactDrawerLayout view) { public void setKeyboardDismissMode(ReactDrawerLayout view, @Nullable String value) {} @Override - @ReactProp(name = "drawerBackgroundColor", customType = "Color") public void setDrawerBackgroundColor(ReactDrawerLayout view, @Nullable Integer value) {} + @ReactProp(name = "drawerBackgroundColor", customType = "Color") + public void setDrawerBackgroundColor(ReactDrawerLayout view, @Nullable Long value) {} + @Override - @ReactProp(name = "statusBarBackgroundColor", customType = "Color") public void setStatusBarBackgroundColor(ReactDrawerLayout view, @Nullable Integer value) {} + @ReactProp(name = "statusBarBackgroundColor", customType = "Color") + public void setStatusBarBackgroundColor(ReactDrawerLayout view, @Nullable Long value) {} + @Override public void setElevation(@NonNull ReactDrawerLayout view, float elevation) { view.setDrawerElevation(PixelUtil.toPixelFromDIP(elevation)); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java index 67345805224b87..d9ce1ed1c90a1d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.java @@ -153,20 +153,20 @@ public void setLoadingIndicatorSource(ReactImageView view, @Nullable String sour } @ReactProp(name = "borderColor", customType = "Color") - public void setBorderColor(ReactImageView view, @Nullable Integer borderColor) { + public void setBorderColor(ReactImageView view, @Nullable Long borderColor) { if (borderColor == null) { - view.setBorderColor(Color.TRANSPARENT); + view.setBorderColor(Color.pack(Color.TRANSPARENT)); } else { view.setBorderColor(borderColor); } } @ReactProp(name = "overlayColor", customType = "Color") - public void setOverlayColor(ReactImageView view, @Nullable Integer overlayColor) { + public void setOverlayColor(ReactImageView view, @Nullable Long overlayColor) { if (overlayColor == null) { view.setOverlayColor(Color.TRANSPARENT); } else { - view.setOverlayColor(overlayColor); + view.setOverlayColor(Color.toArgb(overlayColor)); } } @@ -217,11 +217,11 @@ public void setResizeMethod(ReactImageView view, @Nullable String resizeMethod) } @ReactProp(name = "tintColor", customType = "Color") - public void setTintColor(ReactImageView view, @Nullable Integer tintColor) { + public void setTintColor(ReactImageView view, @Nullable Long tintColor) { if (tintColor == null) { view.clearColorFilter(); } else { - view.setColorFilter(tintColor, Mode.SRC_IN); + view.setColorFilter(Color.toArgb(tintColor), Mode.SRC_IN); } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java index da4fad4b4c7c97..fbf06995564350 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.java @@ -239,9 +239,17 @@ public void setBackgroundColor(int backgroundColor) { } } - public void setBorderColor(int borderColor) { + public void setBackgroundColor(long backgroundColor) { + if (mBackgroundColor != backgroundColor) { + mBackgroundColor = Color.toArgb(backgroundColor); + mBackgroundImageDrawable = new RoundedColorDrawable(Color.toArgb(backgroundColor)); + mIsDirty = true; + } + } + + public void setBorderColor(long borderColor) { if (mBorderColor != borderColor) { - mBorderColor = borderColor; + mBorderColor = Color.toArgb(borderColor); mIsDirty = true; } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java index eb4cbfd779f858..fc5345eef98316 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/progressbar/ReactProgressBarViewManager.java @@ -8,6 +8,7 @@ package com.facebook.react.views.progressbar; import android.content.Context; +import android.graphics.Color; import android.util.Pair; import android.view.View; import android.view.ViewGroup; @@ -88,11 +89,15 @@ public void setStyleAttr(ProgressBarContainerView view, @Nullable String styleNa } @Override - @ReactProp(name = ViewProps.COLOR, customType = "Color") public void setColor(ProgressBarContainerView view, @Nullable Integer color) { view.setColor(color); } + @ReactProp(name = ViewProps.COLOR, customType = "Color") + public void setColor(ProgressBarContainerView view, @Nullable Long color) { + view.setColor(Color.toArgb(color)); + } + @Override @ReactProp(name = PROP_INDETERMINATE) public void setIndeterminate(ProgressBarContainerView view, boolean indeterminate) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 7dbcb788c3ac32..fe27590026d315 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -17,6 +17,7 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -96,7 +97,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private @Nullable FpsListener mFpsListener = null; private @Nullable String mScrollPerfTag; private @Nullable Drawable mEndBackground; - private int mEndFillColor = Color.TRANSPARENT; + private long mEndFillColor = Color.pack(Color.TRANSPARENT); private boolean mDisableIntervalMomentum = false; private int mSnapInterval = 0; private @Nullable List mSnapOffsets; @@ -779,10 +780,9 @@ private View getContentView() { return getChildAt(0); } - public void setEndFillColor(int color) { + public void setEndFillColor(long color) { if (color != mEndFillColor) { mEndFillColor = color; - mEndBackground = new ColorDrawable(mEndFillColor); } } @@ -856,9 +856,10 @@ private boolean isScrollPerfLoggingEnabled() { public void draw(Canvas canvas) { if (mEndFillColor != Color.TRANSPARENT) { final View content = getContentView(); - if (mEndBackground != null && content != null && content.getRight() < getWidth()) { - mEndBackground.setBounds(content.getRight(), 0, getWidth(), getHeight()); - mEndBackground.draw(canvas); + if (content != null && content.getRight() < getWidth()) { + Paint paint = new Paint(); + paint.setColor(mEndFillColor); + canvas.drawRect(content.getRight(), 0, getWidth(), getHeight(), paint); } } super.draw(canvas); @@ -1272,12 +1273,16 @@ public void setBackgroundColor(int color) { mReactBackgroundManager.setBackgroundColor(color); } + public void setBackgroundColor(long color) { + mReactBackgroundManager.setBackgroundColor(color); + } + public void setBorderWidth(int position, float width) { mReactBackgroundManager.setBorderWidth(position, width); } - public void setBorderColor(int position, float color, float alpha) { - mReactBackgroundManager.setBorderColor(position, color, alpha); + public void setBorderColor(int position, long color) { + mReactBackgroundManager.setBorderColor(position, color); } public void setBorderRadius(float borderRadius) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index d311a236ed96f7..ef20e2adcdd165 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -236,7 +236,7 @@ public void scrollToEnd( * @param color */ @ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color") - public void setBottomFillColor(ReactHorizontalScrollView view, int color) { + public void setBottomFillColor(ReactHorizontalScrollView view, long color) { view.setEndFillColor(color); } @@ -291,11 +291,9 @@ public void setBorderWidth(ReactHorizontalScrollView view, int index, float widt "borderBottomColor" }, customType = "Color") - public void setBorderColor(ReactHorizontalScrollView view, int index, Integer color) { - float rgbComponent = - color == null ? YogaConstants.UNDEFINED : (float) ((int) color & 0x00FFFFFF); - float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int) color >>> 24); - view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + public void setBorderColor(ReactHorizontalScrollView view, int index, Long color) { + long borderColor = color == null ? 0 : color; + view.setBorderColor(SPACING_TYPES[index], borderColor); } @ReactProp(name = "overflow") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index f66ed102018ab9..46db99b1cf32ec 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -17,6 +17,7 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -96,7 +97,7 @@ public class ReactScrollView extends ScrollView private @Nullable FpsListener mFpsListener = null; private @Nullable String mScrollPerfTag; private @Nullable Drawable mEndBackground; - private int mEndFillColor = Color.TRANSPARENT; + private long mEndFillColor = Color.pack(Color.TRANSPARENT); private boolean mDisableIntervalMomentum = false; private int mSnapInterval = 0; private @Nullable List mSnapOffsets; @@ -620,9 +621,10 @@ public void setStateWrapper(StateWrapper stateWrapper) { public void draw(Canvas canvas) { if (mEndFillColor != Color.TRANSPARENT) { final View content = getChildAt(0); - if (mEndBackground != null && content != null && content.getBottom() < getHeight()) { - mEndBackground.setBounds(0, content.getBottom(), getWidth(), getHeight()); - mEndBackground.draw(canvas); + if (content != null && content.getBottom() < getHeight()) { + Paint paint = new Paint(); + paint.setColor(mEndFillColor); + canvas.drawRect(0, content.getBottom(), getWidth(), getHeight(), paint); } } getDrawingRect(mRect); @@ -1005,10 +1007,9 @@ private int getSnapInterval() { return getHeight(); } - public void setEndFillColor(int color) { + public void setEndFillColor(long color) { if (color != mEndFillColor) { mEndFillColor = color; - mEndBackground = new ColorDrawable(mEndFillColor); } } @@ -1151,12 +1152,16 @@ public void setBackgroundColor(int color) { mReactBackgroundManager.setBackgroundColor(color); } + public void setBackgroundColor(long color) { + mReactBackgroundManager.setBackgroundColor(color); + } + public void setBorderWidth(int position, float width) { mReactBackgroundManager.setBorderWidth(position, width); } - public void setBorderColor(int position, float color, float alpha) { - mReactBackgroundManager.setBorderColor(position, color, alpha); + public void setBorderColor(int position, long color) { + mReactBackgroundManager.setBorderColor(position, color); } public void setBorderRadius(float borderRadius) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index 1b42aaefefcf92..2f7fc635c150e3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -173,7 +173,7 @@ public void setPagingEnabled(ReactScrollView view, boolean pagingEnabled) { * @param color */ @ReactProp(name = "endFillColor", defaultInt = Color.TRANSPARENT, customType = "Color") - public void setBottomFillColor(ReactScrollView view, int color) { + public void setBottomFillColor(ReactScrollView view, long color) { view.setEndFillColor(color); } @@ -272,10 +272,9 @@ public void setBorderWidth(ReactScrollView view, int index, float width) { "borderBottomColor" }, customType = "Color") - public void setBorderColor(ReactScrollView view, int index, Integer color) { - float rgbComponent = color == null ? YogaConstants.UNDEFINED : (float) (color & 0x00FFFFFF); - float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) (color >>> 24); - view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + public void setBorderColor(ReactScrollView view, int index, Long color) { + long borderColor = color == null ? 0 : color; + view.setBorderColor(SPACING_TYPES[index], borderColor); } @ReactProp(name = "overflow") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java index 255855f3cab4d2..3109fae600ea55 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/SwipeRefreshLayoutManager.java @@ -83,11 +83,15 @@ public void setColors(ReactSwipeRefreshLayout view, @Nullable ReadableArray colo } @Override - @ReactProp(name = "progressBackgroundColor", customType = "Color") public void setProgressBackgroundColor(ReactSwipeRefreshLayout view, Integer color) { view.setProgressBackgroundColorSchemeColor(color == null ? Color.TRANSPARENT : color); } + @ReactProp(name = "progressBackgroundColor", customType = "Color") + public void setProgressBackgroundColor(ReactSwipeRefreshLayout view, Long color) { + view.setProgressBackgroundColorSchemeColor(color == null ? Color.TRANSPARENT : Color.toArgb(color)); + } + // TODO(T46143833): Remove this method once the 'size' prop has been migrated to String in JS. public void setSize(ReactSwipeRefreshLayout view, int value) { view.setSize(value); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java index 2bdd48ecc406b6..c0240bb3dc9b39 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -55,6 +56,12 @@ public void setBackgroundColor(int color) { createRippleDrawableColorStateList(color), new ColorDrawable(color), null)); } + public void setBackgroundColor(long color) { + setBackground( + new RippleDrawable( + createRippleDrawableColorStateList(Color.toArgb(color)), new ColorDrawable(Color.toArgb(color)), null)); + } + void setColor(Drawable drawable, @Nullable Integer color) { if (color == null) { drawable.clearColorFilter(); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java index 9fc2d32315eab5..57664025c9353a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java @@ -9,6 +9,7 @@ package com.facebook.react.views.switchview; import android.content.Context; +import android.graphics.Color; import android.view.View; import android.widget.CompoundButton; import androidx.annotation.NonNull; @@ -142,35 +143,55 @@ public void setValue(ReactSwitch view, boolean value) { } @Override - @ReactProp(name = "thumbTintColor", customType = "Color") public void setThumbTintColor(ReactSwitch view, @Nullable Integer color) { this.setThumbColor(view, color); } + @ReactProp(name = "thumbTintColor", customType = "Color") + public void setThumbTintColor(ReactSwitch view, @Nullable Long color) { + this.setThumbColor(view, Color.toArgb(color)); + } + @Override - @ReactProp(name = "thumbColor", customType = "Color") public void setThumbColor(ReactSwitch view, @Nullable Integer color) { view.setThumbColor(color); } + @ReactProp(name = "thumbColor", customType = "Color") + public void setThumbColor(ReactSwitch view, @Nullable Long color) { + view.setThumbColor(Color.toArgb(color)); + } + @Override - @ReactProp(name = "trackColorForFalse", customType = "Color") public void setTrackColorForFalse(ReactSwitch view, @Nullable Integer color) { view.setTrackColorForFalse(color); } + @ReactProp(name = "trackColorForFalse", customType = "Color") + public void setTrackColorForFalse(ReactSwitch view, @Nullable Long color) { + view.setTrackColorForFalse(Color.toArgb(color)); + } + @Override - @ReactProp(name = "trackColorForTrue", customType = "Color") public void setTrackColorForTrue(ReactSwitch view, @Nullable Integer color) { view.setTrackColorForTrue(color); } + @ReactProp(name = "trackColorForTrue", customType = "Color") + public void setTrackColorForTrue(ReactSwitch view, @Nullable Long color) { + view.setTrackColorForTrue(Color.toArgb(color)); + } + @Override - @ReactProp(name = "trackTintColor", customType = "Color") public void setTrackTintColor(ReactSwitch view, @Nullable Integer color) { view.setTrackColor(color); } + @ReactProp(name = "trackTintColor", customType = "Color") + public void setTrackTintColor(ReactSwitch view, @Nullable Long color) { + view.setTrackColor(Color.toArgb(color)); + } + @Override public void setNativeValue(ReactSwitch view, boolean value) { setValueInternal(view, value); @@ -223,4 +244,4 @@ private static void setValueInternal(ReactSwitch view, boolean value) { view.setOn(value); view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); } -} +} \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt index bf9693f5971514..8ad8a597ca2824 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/BasicTextAttributeProvider.kt @@ -20,11 +20,11 @@ internal interface BasicTextAttributeProvider { val isBackgroundColorSet: Boolean - val backgroundColor: Int + val backgroundColor: Long val isColorSet: Boolean - val color: Int + val color: Long val fontStyle: Int @@ -44,5 +44,5 @@ internal interface BasicTextAttributeProvider { val textShadowRadius: Float - val textShadowColor: Int + val textShadowColor: Long } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java index c5cabecf7047f6..61909190746bc6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java @@ -427,9 +427,9 @@ protected Spannable spannedFromShadowNode( protected TextAttributes mTextAttributes; protected boolean mIsColorSet = false; - protected int mColor; + protected long mColor; protected boolean mIsBackgroundColorSet = false; - protected int mBackgroundColor; + protected long mBackgroundColor; protected @Nullable AccessibilityRole mAccessibilityRole = null; protected @Nullable Role mRole = null; @@ -444,7 +444,7 @@ protected Spannable spannedFromShadowNode( protected float mTextShadowOffsetDx = 0; protected float mTextShadowOffsetDy = 0; protected float mTextShadowRadius = 0; - protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR; + protected long mTextShadowColor = Color.pack(DEFAULT_TEXT_SHADOW_COLOR); protected boolean mIsUnderlineTextDecorationSet = false; protected boolean mIsLineThroughTextDecorationSet = false; @@ -580,12 +580,12 @@ public void setFontSize(float fontSize) { } @Override - public int getColor() { + public long getColor() { return mColor; } @ReactProp(name = ViewProps.COLOR, customType = "Color") - public void setColor(@Nullable Integer color) { + public void setColor(@Nullable Long color) { mIsColorSet = (color != null); if (mIsColorSet) { mColor = color; @@ -599,12 +599,12 @@ public boolean isColorSet() { } @Override - public int getBackgroundColor() { + public long getBackgroundColor() { return mBackgroundColor; } @ReactProp(name = ViewProps.BACKGROUND_COLOR, customType = "Color") - public void setBackgroundColor(@Nullable Integer color) { + public void setBackgroundColor(@Nullable Long color) { // Background color needs to be handled here for virtual nodes so it can be incorporated into // the span. However, it doesn't need to be applied to non-virtual nodes because non-virtual // nodes get mapped to native views and native views get their background colors get set via @@ -795,12 +795,12 @@ public void setTextShadowRadius(float textShadowRadius) { } @Override - public int getTextShadowColor() { + public long getTextShadowColor() { return mTextShadowColor; } @ReactProp(name = PROP_SHADOW_COLOR, defaultInt = DEFAULT_TEXT_SHADOW_COLOR, customType = "Color") - public void setTextShadowColor(int textShadowColor) { + public void setTextShadowColor(long textShadowColor) { if (textShadowColor != mTextShadowColor) { mTextShadowColor = textShadowColor; markUpdated(); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java index deb5097c15f23b..d3587d09a5d366 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import android.graphics.Color; import android.text.Layout; import android.text.Spannable; import android.text.TextUtils; @@ -106,12 +107,12 @@ public void setSelectable(ReactTextView view, boolean isSelectable) { } @ReactProp(name = "selectionColor", customType = "Color") - public void setSelectionColor(ReactTextView view, @Nullable Integer color) { + public void setSelectionColor(ReactTextView view, @Nullable Long color) { if (color == null) { view.setHighlightColor( DefaultStyleValuesUtil.getDefaultTextColorHighlight(view.getContext())); } else { - view.setHighlightColor(color); + view.setHighlightColor(Color.toArgb(color)); } } @@ -180,11 +181,9 @@ public void setBorderWidth(ReactTextView view, int index, float width) { "borderBottomColor" }, customType = "Color") - public void setBorderColor(ReactTextView view, int index, Integer color) { - float rgbComponent = - color == null ? YogaConstants.UNDEFINED : (float) ((int) color & 0x00FFFFFF); - float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int) color >>> 24); - view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + public void setBorderColor(ReactTextView view, int index, Long color) { + long borderColor = color == null ? 0 : color; + view.setBorderColor(SPACING_TYPES[index], borderColor); } @ReactProp(name = ViewProps.INCLUDE_FONT_PADDING, defaultBoolean = true) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index cf50efcbfa1a0a..7dd47ee51431ef 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -626,12 +626,16 @@ public void setBackgroundColor(int color) { mReactBackgroundManager.setBackgroundColor(color); } + public void setBackgroundColor(long color) { + mReactBackgroundManager.setBackgroundColor(color); + } + public void setBorderWidth(int position, float width) { mReactBackgroundManager.setBorderWidth(position, width); } - public void setBorderColor(int position, float color, float alpha) { - mReactBackgroundManager.setBorderColor(position, color, alpha); + public void setBorderColor(int position, long color) { + mReactBackgroundManager.setBorderColor(position, color); } public void setBorderRadius(float borderRadius) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 25791e00679b34..6ee45e59f41943 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text; +import android.graphics.Color; import android.os.Build; import android.text.Layout; import android.text.TextUtils; @@ -70,7 +71,7 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { private static final String PROP_TEXT_TRANSFORM = "textTransform"; - private static final int DEFAULT_TEXT_SHADOW_COLOR = 0x55000000; + private static final long DEFAULT_TEXT_SHADOW_COLOR = Color.pack(0x55000000); private static final int DEFAULT_JUSTIFICATION_MODE = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) ? 0 : Layout.JUSTIFICATION_MODE_NONE; private static final int DEFAULT_BREAK_STRATEGY = Layout.BREAK_STRATEGY_HIGH_QUALITY; @@ -79,9 +80,9 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { protected float mLineHeight = Float.NaN; protected boolean mIsColorSet = false; protected boolean mAllowFontScaling = true; - protected int mColor; + protected long mColor; protected boolean mIsBackgroundColorSet = false; - protected int mBackgroundColor; + protected long mBackgroundColor; protected int mNumberOfLines = ReactConstants.UNSET; protected int mFontSize = ReactConstants.UNSET; @@ -98,7 +99,7 @@ public class TextAttributeProps implements EffectiveTextAttributeProvider { protected float mTextShadowOffsetDx = 0; protected float mTextShadowOffsetDy = 0; protected float mTextShadowRadius = 0; - protected int mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR; + protected long mTextShadowColor = DEFAULT_TEXT_SHADOW_COLOR; protected boolean mIsUnderlineTextDecorationSet = false; protected boolean mIsLineThroughTextDecorationSet = false; @@ -150,10 +151,10 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { MapBuffer.Entry entry = iterator.next(); switch (entry.getKey()) { case TA_KEY_FOREGROUND_COLOR: - result.setColor(entry.getIntValue()); + result.setColor(entry.getLongValue()); break; case TA_KEY_BACKGROUND_COLOR: - result.setBackgroundColor(entry.getIntValue()); + result.setBackgroundColor(entry.getLongValue()); break; case TA_KEY_OPACITY: break; @@ -198,7 +199,7 @@ public static TextAttributeProps fromMapBuffer(MapBuffer props) { result.setTextShadowRadius((float) entry.getDoubleValue()); break; case TA_KEY_TEXT_SHADOW_COLOR: - result.setTextShadowColor(entry.getIntValue()); + result.setTextShadowColor(entry.getLongValue()); break; case TA_KEY_TEXT_SHADOW_OFFSET_DX: result.setTextShadowOffsetDx((float) entry.getDoubleValue()); @@ -237,14 +238,14 @@ public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, ReactConstants.UNSET)); - result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); + result.setColor(props.hasKey(ViewProps.COLOR) ? props.getLong(ViewProps.COLOR, 0) : null); result.setColor( props.hasKey(ViewProps.FOREGROUND_COLOR) - ? props.getInt(ViewProps.FOREGROUND_COLOR, 0) + ? props.getLong(ViewProps.FOREGROUND_COLOR, 0) : null); result.setBackgroundColor( props.hasKey(ViewProps.BACKGROUND_COLOR) - ? props.getInt(ViewProps.BACKGROUND_COLOR, 0) + ? props.getLong(ViewProps.BACKGROUND_COLOR, 0) : null); result.setFontFamily(getStringProp(props, ViewProps.FONT_FAMILY)); result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT)); @@ -255,7 +256,7 @@ public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { result.setTextShadowOffset( props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null); result.setTextShadowRadius(getFloatProp(props, PROP_SHADOW_RADIUS, 1)); - result.setTextShadowColor(getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); + result.setTextShadowColor(getLongProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM)); result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION)); result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE)); @@ -324,6 +325,14 @@ private static int getIntProp(ReactStylesDiffMap mProps, String name, int defaul } } + private static long getLongProp(ReactStylesDiffMap mProps, String name, long defaultvalue) { + if (mProps.hasKey(name)) { + return mProps.getLong(name, defaultvalue); + } else { + return defaultvalue; + } + } + private static float getFloatProp(ReactStylesDiffMap mProps, String name, float defaultvalue) { if (mProps.hasKey(name)) { return mProps.getFloat(name, defaultvalue); @@ -423,11 +432,11 @@ private void setFontSize(float fontSize) { } @Override - public int getColor() { + public long getColor() { return mColor; } - private void setColor(@Nullable Integer color) { + private void setColor(@Nullable Long color) { mIsColorSet = (color != null); if (mIsColorSet) { mColor = color; @@ -440,11 +449,11 @@ public boolean isColorSet() { } @Override - public int getBackgroundColor() { + public long getBackgroundColor() { return mBackgroundColor; } - private void setBackgroundColor(Integer color) { + private void setBackgroundColor(Long color) { // TODO: Don't apply background color to anchor TextView since it will be applied on the View // directly // if (!isVirtualAnchor()) { @@ -685,11 +694,11 @@ private void setTextShadowRadius(float textShadowRadius) { } @Override - public int getTextShadowColor() { + public long getTextShadowColor() { return mTextShadowColor; } - private void setTextShadowColor(int textShadowColor) { + private void setTextShadowColor(long textShadowColor) { if (textShadowColor != mTextShadowColor) { mTextShadowColor = textShadowColor; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt index e72afed9458222..adfbe3768189c1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutUtils.kt @@ -339,7 +339,7 @@ internal object TextLayoutUtils { textAttributeProvider.textShadowOffsetDx != 0f || textAttributeProvider.textShadowOffsetDy != 0f val hasTextShadowRadius = textAttributeProvider.textShadowRadius != 0f - val hasTextShadowColorAlpha = Color.alpha(textAttributeProvider.textShadowColor) != 0 + val hasTextShadowColorAlpha = Color.alpha(textAttributeProvider.textShadowColor) != 0f if ((hasTextShadowOffset || hasTextShadowRadius) && hasTextShadowColorAlpha) { ops.add( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java index a512b20833caca..0498f6323e566d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/frescosupport/FrescoBasedReactTextInlineImageShadowNode.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.res.Resources; +import android.graphics.Color; import android.net.Uri; import androidx.annotation.Nullable; import com.facebook.common.logging.FLog; @@ -75,8 +76,8 @@ public void setHeaders(ReadableMap headers) { } @ReactProp(name = "tintColor", customType = "Color") - public void setTintColor(int tintColor) { - mTintColor = tintColor; + public void setTintColor(long tintColor) { + mTintColor = Color.toArgb(tintColor); } /** Besides width/height, all other layout props on inline images are ignored */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundColorSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundColorSpan.java index a04f2439dffe64..e00324d0dcbff5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundColorSpan.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactBackgroundColorSpan.java @@ -7,6 +7,7 @@ package com.facebook.react.views.text.internal.span; +import android.graphics.Color; import android.text.style.BackgroundColorSpan; /* @@ -16,4 +17,8 @@ public class ReactBackgroundColorSpan extends BackgroundColorSpan implements Rea public ReactBackgroundColorSpan(int color) { super(color); } -} + + public ReactBackgroundColorSpan(long color) { + super(Color.toArgb(color)); + } +} \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactForegroundColorSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactForegroundColorSpan.java index 0a7b489b9230c3..9be15c90ccdc66 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactForegroundColorSpan.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ReactForegroundColorSpan.java @@ -7,13 +7,31 @@ package com.facebook.react.views.text.internal.span; +import android.graphics.Color; import android.text.style.ForegroundColorSpan; +import android.text.TextPaint; +import androidx.annotation.NonNull; /* * Wraps {@link ForegroundColorSpan} as a {@link ReactSpan}. */ public class ReactForegroundColorSpan extends ForegroundColorSpan implements ReactSpan { + private long mColor = 0; + public ReactForegroundColorSpan(int color) { super(color); } -} + + public ReactForegroundColorSpan(long color) { + super(Color.toArgb(color)); + this.mColor = color; + } + + @Override + public void updateDrawState(@NonNull TextPaint tp) { + super.updateDrawState(tp); + if (mColor != 0) { + tp.setColor(mColor); + } + } +} \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.java index 63325f32638b12..91c9192dcacb27 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/ShadowStyleSpan.java @@ -7,14 +7,23 @@ package com.facebook.react.views.text.internal.span; +import android.graphics.Color; +import android.graphics.ColorSpace; import android.text.TextPaint; import android.text.style.CharacterStyle; public class ShadowStyleSpan extends CharacterStyle implements ReactSpan { private final float mDx, mDy, mRadius; - private final int mColor; + private final long mColor; public ShadowStyleSpan(float dx, float dy, float radius, int color) { + mDx = dx; + mDy = dy; + mRadius = radius; + mColor = Color.pack(color); + } + + public ShadowStyleSpan(float dx, float dy, float radius, long color) { mDx = dx; mDy = dy; mRadius = radius; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java index 0904f430a68da4..6a08f95eaa4c01 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java @@ -1068,15 +1068,19 @@ public void setBackgroundColor(int color) { mReactBackgroundManager.setBackgroundColor(color); } + public void setBackgroundColor(long color) { + mReactBackgroundManager.setBackgroundColor(color); + } + public void setBorderWidth(int position, float width) { mReactBackgroundManager.setBorderWidth(position, width); } - public void setBorderColor(int position, float color, float alpha) { - mReactBackgroundManager.setBorderColor(position, color, alpha); + public void setBorderColor(int position, long color) { + mReactBackgroundManager.setBorderColor(position, color); } - public int getBorderColor(int position) { + public long getBorderColor(int position) { return mReactBackgroundManager.getBorderColor(position); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 0915a49a0bb214..8f77a9dd3b1cee 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -13,6 +13,7 @@ import android.content.res.ColorStateList; import android.graphics.BlendMode; import android.graphics.BlendModeColorFilter; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -509,32 +510,32 @@ public void setPlaceholder(ReactEditText view, String placeholder) { } @ReactProp(name = "placeholderTextColor", customType = "Color") - public void setPlaceholderTextColor(ReactEditText view, @Nullable Integer color) { + public void setPlaceholderTextColor(ReactEditText view, @Nullable Long color) { if (color == null) { view.setHintTextColor(DefaultStyleValuesUtil.getDefaultTextColorHint(view.getContext())); } else { - view.setHintTextColor(color); + view.setHintTextColor(Color.toArgb(color)); } } @ReactProp(name = "selectionColor", customType = "Color") - public void setSelectionColor(ReactEditText view, @Nullable Integer color) { + public void setSelectionColor(ReactEditText view, @Nullable Long color) { if (color == null) { view.setHighlightColor( DefaultStyleValuesUtil.getDefaultTextColorHighlight(view.getContext())); } else { - view.setHighlightColor(color); + view.setHighlightColor(Color.toArgb(color)); } } @ReactProp(name = "selectionHandleColor", customType = "Color") - public void setSelectionHandleColor(ReactEditText view, @Nullable Integer color) { + public void setSelectionHandleColor(ReactEditText view, @Nullable Long color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Drawable drawableCenter = view.getTextSelectHandle().mutate(); Drawable drawableLeft = view.getTextSelectHandleLeft().mutate(); Drawable drawableRight = view.getTextSelectHandleRight().mutate(); if (color != null) { - BlendModeColorFilter filter = new BlendModeColorFilter(color, BlendMode.SRC_IN); + BlendModeColorFilter filter = new BlendModeColorFilter(Color.toArgb(color), BlendMode.SRC_IN); drawableCenter.setColorFilter(filter); drawableLeft.setColorFilter(filter); drawableRight.setColorFilter(filter); @@ -569,7 +570,7 @@ public void setSelectionHandleColor(ReactEditText view, @Nullable Integer color) Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate(); if (color != null) { - drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawable.setColorFilter(Color.toArgb(color), PorterDuff.Mode.SRC_IN); } else { drawable.clearColorFilter(); } @@ -588,12 +589,12 @@ public void setSelectionHandleColor(ReactEditText view, @Nullable Integer color) } @ReactProp(name = "cursorColor", customType = "Color") - public void setCursorColor(ReactEditText view, @Nullable Integer color) { + public void setCursorColor(ReactEditText view, @Nullable Long color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Drawable cursorDrawable = view.getTextCursorDrawable(); if (cursorDrawable != null) { if (color != null) { - cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN)); + cursorDrawable.setColorFilter(new BlendModeColorFilter(Color.toArgb(color), BlendMode.SRC_IN)); } else { cursorDrawable.clearColorFilter(); } @@ -623,7 +624,7 @@ public void setCursorColor(ReactEditText view, @Nullable Integer color) { Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate(); if (color != null) { - drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawable.setColorFilter(Color.toArgb(color), PorterDuff.Mode.SRC_IN); } else { drawable.clearColorFilter(); } @@ -674,7 +675,7 @@ public void setSelectTextOnFocus(ReactEditText view, boolean selectTextOnFocus) } @ReactProp(name = ViewProps.COLOR, customType = "Color") - public void setColor(ReactEditText view, @Nullable Integer color) { + public void setColor(ReactEditText view, @Nullable Long color) { if (color == null) { ColorStateList defaultContextTextColor = DefaultStyleValuesUtil.getDefaultTextColor(view.getContext()); @@ -690,12 +691,12 @@ public void setColor(ReactEditText view, @Nullable Integer color) { + (c != null ? c.getClass().getCanonicalName() : "null"))); } } else { - view.setTextColor(color); + view.setTextColor(Color.toArgb(color)); } } @ReactProp(name = "underlineColorAndroid", customType = "Color") - public void setUnderlineColor(ReactEditText view, @Nullable Integer underlineColor) { + public void setUnderlineColor(ReactEditText view, @Nullable Long underlineColor) { // Drawable.mutate() can sometimes crash due to an AOSP bug: // See https://code.google.com/p/android/issues/detail?id=191754 for more info Drawable background = view.getBackground(); @@ -719,12 +720,12 @@ public void setUnderlineColor(ReactEditText view, @Nullable Integer underlineCol // fixes underlineColor transparent not working on API 21 // re-sets the TextInput underlineColor https://bit.ly/3M4alr6 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { - int bottomBorderColor = view.getBorderColor(Spacing.BOTTOM); + long bottomBorderColor = view.getBorderColor(Spacing.BOTTOM); setBorderColor(view, Spacing.START, underlineColor); - drawableToMutate.setColorFilter(underlineColor, PorterDuff.Mode.SRC_IN); + drawableToMutate.setColorFilter(Color.toArgb(underlineColor), PorterDuff.Mode.SRC_IN); setBorderColor(view, Spacing.START, bottomBorderColor); } else { - drawableToMutate.setColorFilter(underlineColor, PorterDuff.Mode.SRC_IN); + drawableToMutate.setColorFilter(Color.toArgb(underlineColor), PorterDuff.Mode.SRC_IN); } } } @@ -1038,11 +1039,9 @@ public void setBorderWidth(ReactEditText view, int index, float width) { "borderBottomColor" }, customType = "Color") - public void setBorderColor(ReactEditText view, int index, Integer color) { - float rgbComponent = - color == null ? YogaConstants.UNDEFINED : (float) ((int) color & 0x00FFFFFF); - float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int) color >>> 24); - view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + public void setBorderColor(ReactEditText view, int index, Long color) { + long borderColor = color == null ? 0 : color; + view.setBorderColor(SPACING_TYPES[index], borderColor); } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java index c93128a89aa270..9c33d5066a7d05 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundDrawable.java @@ -5,1435 +5,1434 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.views.view; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.DashPathEffect; -import android.graphics.Outline; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PathEffect; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Region; -import android.graphics.drawable.Drawable; -import android.view.View; -import androidx.annotation.Nullable; -import com.facebook.react.common.annotations.VisibleForTesting; -import com.facebook.react.modules.i18nmanager.I18nUtil; -import com.facebook.react.uimanager.FloatUtil; -import com.facebook.react.uimanager.Spacing; -import com.facebook.yoga.YogaConstants; -import java.util.Arrays; -import java.util.Locale; - -/** - * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports drawing - * background color and borders (including rounded borders) by providing a react friendly API - * (setter for each of those properties). - * - *

The implementation tries to allocate as few objects as possible depending on which properties - * are set. E.g. for views with rounded background/borders we allocate {@code - * mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view - * have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only - * background color is set we won't allocate any extra/unnecessary objects. - */ -public class ReactViewBackgroundDrawable extends Drawable { - - private static final int DEFAULT_BORDER_COLOR = Color.BLACK; - private static final int DEFAULT_BORDER_RGB = 0x00FFFFFF & DEFAULT_BORDER_COLOR; - private static final int DEFAULT_BORDER_ALPHA = (0xFF000000 & DEFAULT_BORDER_COLOR) >>> 24; - // ~0 == 0xFFFFFFFF, all bits set to 1. - private static final int ALL_BITS_SET = ~0; - // 0 == 0x00000000, all bits set to 0. - private static final int ALL_BITS_UNSET = 0; - - private enum BorderStyle { - SOLID, - DASHED, - DOTTED; - - public static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { - switch (style) { - case SOLID: - return null; - - case DASHED: - return new DashPathEffect( - new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); - - case DOTTED: - return new DashPathEffect( - new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); - - default: - return null; - } - } - }; - - /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ - private @Nullable Spacing mBorderWidth; - private @Nullable Spacing mBorderRGB; - private @Nullable Spacing mBorderAlpha; - private @Nullable BorderStyle mBorderStyle; - - private @Nullable Path mInnerClipPathForBorderRadius; - private @Nullable Path mBackgroundColorRenderPath; - private @Nullable Path mOuterClipPathForBorderRadius; - private @Nullable Path mPathForBorderRadiusOutline; - private @Nullable Path mPathForBorder; - private final Path mPathForSingleBorder = new Path(); - private @Nullable Path mCenterDrawPath; - private @Nullable RectF mInnerClipTempRectForBorderRadius; - private @Nullable RectF mOuterClipTempRectForBorderRadius; - private @Nullable RectF mTempRectForBorderRadiusOutline; - private @Nullable RectF mTempRectForCenterDrawPath; - private @Nullable PointF mInnerTopLeftCorner; - private @Nullable PointF mInnerTopRightCorner; - private @Nullable PointF mInnerBottomRightCorner; - private @Nullable PointF mInnerBottomLeftCorner; - private boolean mNeedUpdatePathForBorderRadius = false; - private float mBorderRadius = YogaConstants.UNDEFINED; - - /* Used by all types of background and for drawing borders */ - private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private int mColor = Color.TRANSPARENT; - private int mAlpha = 255; - - // There is a small gap between the edges of adjacent paths - // such as between the mBackgroundColorRenderPath and its border. - // The smallest amount (found to be 0.8f) is used to extend - // the paths, overlapping them and closing the visible gap. - private final float mGapBetweenPaths = 0.8f; - - private @Nullable float[] mBorderCornerRadii; - private final Context mContext; - private int mLayoutDirection; - - public enum BorderRadiusLocation { - TOP_LEFT, - TOP_RIGHT, - BOTTOM_RIGHT, - BOTTOM_LEFT, - TOP_START, - TOP_END, - BOTTOM_START, - BOTTOM_END, - END_END, - END_START, - START_END, - START_START - } - - public ReactViewBackgroundDrawable(Context context) { - mContext = context; - } - - @Override - public void draw(Canvas canvas) { - updatePathEffect(); - if (!hasRoundedBorders()) { - drawRectangularBackgroundWithBorders(canvas); - } else { - drawRoundedBackgroundWithBorders(canvas); - } - } - - public boolean hasRoundedBorders() { - if (!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) { - return true; - } - - if (mBorderCornerRadii != null) { - for (final float borderRadii : mBorderCornerRadii) { - if (!YogaConstants.isUndefined(borderRadii) && borderRadii > 0) { - return true; - } - } - } - - return false; - } - - @Override - protected void onBoundsChange(Rect bounds) { - super.onBoundsChange(bounds); - mNeedUpdatePathForBorderRadius = true; - } - - @Override - public void setAlpha(int alpha) { - if (alpha != mAlpha) { - mAlpha = alpha; - invalidateSelf(); - } - } - - @Override - public int getAlpha() { - return mAlpha; - } - - @Override - public void setColorFilter(ColorFilter cf) { - // do nothing - } - - @Override - public int getOpacity() { - return ColorUtil.getOpacityFromColor(ColorUtil.multiplyColorAlpha(mColor, mAlpha)); - } - - /* Android's elevation implementation requires this to be implemented to know where to draw the shadow. */ - @Override - public void getOutline(Outline outline) { - if ((!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) - || mBorderCornerRadii != null) { - updatePath(); - - outline.setConvexPath(mPathForBorderRadiusOutline); - } else { - outline.setRect(getBounds()); - } - } - - public void setBorderWidth(int position, float width) { - if (mBorderWidth == null) { - mBorderWidth = new Spacing(); - } - if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { - mBorderWidth.set(position, width); - switch (position) { - case Spacing.ALL: - case Spacing.LEFT: - case Spacing.BOTTOM: - case Spacing.RIGHT: - case Spacing.TOP: - case Spacing.START: - case Spacing.END: - mNeedUpdatePathForBorderRadius = true; - } - invalidateSelf(); - } - } - - public void setBorderColor(int position, float rgb, float alpha) { - this.setBorderRGB(position, rgb); - this.setBorderAlpha(position, alpha); - mNeedUpdatePathForBorderRadius = true; - } - - private void setBorderRGB(int position, float rgb) { - // set RGB component - if (mBorderRGB == null) { - mBorderRGB = new Spacing(DEFAULT_BORDER_RGB); - } - if (!FloatUtil.floatsEqual(mBorderRGB.getRaw(position), rgb)) { - mBorderRGB.set(position, rgb); - invalidateSelf(); - } - } - - private void setBorderAlpha(int position, float alpha) { - // set Alpha component - if (mBorderAlpha == null) { - mBorderAlpha = new Spacing(DEFAULT_BORDER_ALPHA); - } - if (!FloatUtil.floatsEqual(mBorderAlpha.getRaw(position), alpha)) { - mBorderAlpha.set(position, alpha); - invalidateSelf(); - } - } - - public void setBorderStyle(@Nullable String style) { - BorderStyle borderStyle = - style == null ? null : BorderStyle.valueOf(style.toUpperCase(Locale.US)); - if (mBorderStyle != borderStyle) { - mBorderStyle = borderStyle; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public void setRadius(float radius) { - if (!FloatUtil.floatsEqual(mBorderRadius, radius)) { - mBorderRadius = radius; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public void setRadius(float radius, int position) { - if (mBorderCornerRadii == null) { - mBorderCornerRadii = new float[12]; - Arrays.fill(mBorderCornerRadii, YogaConstants.UNDEFINED); - } - - if (!FloatUtil.floatsEqual(mBorderCornerRadii[position], radius)) { - mBorderCornerRadii[position] = radius; - mNeedUpdatePathForBorderRadius = true; - invalidateSelf(); - } - } - - public float getFullBorderRadius() { - return YogaConstants.isUndefined(mBorderRadius) ? 0 : mBorderRadius; - } - - public float getBorderRadius(final BorderRadiusLocation location) { - return getBorderRadiusOrDefaultTo(YogaConstants.UNDEFINED, location); - } - - public float getBorderRadiusOrDefaultTo( - final float defaultValue, final BorderRadiusLocation location) { - if (mBorderCornerRadii == null) { - return defaultValue; - } - - final float radius = mBorderCornerRadii[location.ordinal()]; - - if (YogaConstants.isUndefined(radius)) { - return defaultValue; - } - - return radius; - } - - public void setColor(int color) { - mColor = color; - invalidateSelf(); - } - - /** Similar to Drawable.getLayoutDirection, but available in APIs < 23. */ - public int getResolvedLayoutDirection() { - return mLayoutDirection; - } - - /** Similar to Drawable.setLayoutDirection, but available in APIs < 23. */ - public boolean setResolvedLayoutDirection(int layoutDirection) { - if (mLayoutDirection != layoutDirection) { - mLayoutDirection = layoutDirection; - return onResolvedLayoutDirectionChanged(layoutDirection); - } - return false; - } - - /** Similar to Drawable.onLayoutDirectionChanged, but available in APIs < 23. */ - public boolean onResolvedLayoutDirectionChanged(int layoutDirection) { - return false; - } - - @VisibleForTesting - public int getColor() { - return mColor; - } - - private void drawRoundedBackgroundWithBorders(Canvas canvas) { - updatePath(); - canvas.save(); - - // Clip outer border - canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); - - // Draws the View without its border first (with background color fill) - int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); - if (Color.alpha(useColor) != 0) { // color is not transparent - mPaint.setColor(useColor); - mPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(mBackgroundColorRenderPath, mPaint); - } - - final RectF borderWidth = getDirectionAwareBorderInsets(); - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - if (borderWidth.top > 0 - || borderWidth.bottom > 0 - || borderWidth.left > 0 - || borderWidth.right > 0) { - - // If it's a full and even border draw inner rect path with stroke - final float fullBorderWidth = getFullBorderWidth(); - int borderColor = getBorderColor(Spacing.ALL); - if (borderWidth.top == fullBorderWidth - && borderWidth.bottom == fullBorderWidth - && borderWidth.left == fullBorderWidth - && borderWidth.right == fullBorderWidth - && colorLeft == borderColor - && colorTop == borderColor - && colorRight == borderColor - && colorBottom == borderColor) { - if (fullBorderWidth > 0) { - mPaint.setColor(ColorUtil.multiplyColorAlpha(borderColor, mAlpha)); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(fullBorderWidth); - canvas.drawPath(mCenterDrawPath, mPaint); - } - } - // In the case of uneven border widths/colors draw quadrilateral in each direction - else { - mPaint.setStyle(Paint.Style.FILL); - - // Clip inner border - canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - int colorStart = getBorderColor(Spacing.START); - int colorEnd = getBorderColor(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (!isBorderColorDefined(Spacing.START)) { - colorStart = colorLeft; - } - - if (!isBorderColorDefined(Spacing.END)) { - colorEnd = colorRight; - } - - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - colorLeft = directionAwareColorLeft; - colorRight = directionAwareColorRight; - } else { - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); - final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); - final boolean isDirectionAwareColorLeftDefined = - isRTL ? isColorEndDefined : isColorStartDefined; - final boolean isDirectionAwareColorRightDefined = - isRTL ? isColorStartDefined : isColorEndDefined; - - if (isDirectionAwareColorLeftDefined) { - colorLeft = directionAwareColorLeft; - } - - if (isDirectionAwareColorRightDefined) { - colorRight = directionAwareColorRight; - } - } - - final float left = mOuterClipTempRectForBorderRadius.left; - final float right = mOuterClipTempRectForBorderRadius.right; - final float top = mOuterClipTempRectForBorderRadius.top; - final float bottom = mOuterClipTempRectForBorderRadius.bottom; - - // mGapBetweenPaths is used to close the gap between the diagonal - // edges of the quadrilaterals on adjacent sides of the rectangle - if (borderWidth.left > 0) { - final float x1 = left; - final float y1 = top - mGapBetweenPaths; - final float x2 = mInnerTopLeftCorner.x; - final float y2 = mInnerTopLeftCorner.y - mGapBetweenPaths; - final float x3 = mInnerBottomLeftCorner.x; - final float y3 = mInnerBottomLeftCorner.y + mGapBetweenPaths; - final float x4 = left; - final float y4 = bottom + mGapBetweenPaths; - - drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.top > 0) { - final float x1 = left - mGapBetweenPaths; - final float y1 = top; - final float x2 = mInnerTopLeftCorner.x - mGapBetweenPaths; - final float y2 = mInnerTopLeftCorner.y; - final float x3 = mInnerTopRightCorner.x + mGapBetweenPaths; - final float y3 = mInnerTopRightCorner.y; - final float x4 = right + mGapBetweenPaths; - final float y4 = top; - - drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.right > 0) { - final float x1 = right; - final float y1 = top - mGapBetweenPaths; - final float x2 = mInnerTopRightCorner.x; - final float y2 = mInnerTopRightCorner.y - mGapBetweenPaths; - final float x3 = mInnerBottomRightCorner.x; - final float y3 = mInnerBottomRightCorner.y + mGapBetweenPaths; - final float x4 = right; - final float y4 = bottom + mGapBetweenPaths; - - drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderWidth.bottom > 0) { - final float x1 = left - mGapBetweenPaths; - final float y1 = bottom; - final float x2 = mInnerBottomLeftCorner.x - mGapBetweenPaths; - final float y2 = mInnerBottomLeftCorner.y; - final float x3 = mInnerBottomRightCorner.x + mGapBetweenPaths; - final float y3 = mInnerBottomRightCorner.y; - final float x4 = right + mGapBetweenPaths; - final float y4 = bottom; - - drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); - } - } - } - - canvas.restore(); - } - - private void updatePath() { - if (!mNeedUpdatePathForBorderRadius) { - return; - } - - mNeedUpdatePathForBorderRadius = false; - - if (mInnerClipPathForBorderRadius == null) { - mInnerClipPathForBorderRadius = new Path(); - } - - if (mBackgroundColorRenderPath == null) { - mBackgroundColorRenderPath = new Path(); - } - - if (mOuterClipPathForBorderRadius == null) { - mOuterClipPathForBorderRadius = new Path(); - } - - if (mPathForBorderRadiusOutline == null) { - mPathForBorderRadiusOutline = new Path(); - } - - if (mCenterDrawPath == null) { - mCenterDrawPath = new Path(); - } - - if (mInnerClipTempRectForBorderRadius == null) { - mInnerClipTempRectForBorderRadius = new RectF(); - } - - if (mOuterClipTempRectForBorderRadius == null) { - mOuterClipTempRectForBorderRadius = new RectF(); - } - - if (mTempRectForBorderRadiusOutline == null) { - mTempRectForBorderRadiusOutline = new RectF(); - } - - if (mTempRectForCenterDrawPath == null) { - mTempRectForCenterDrawPath = new RectF(); - } - - mInnerClipPathForBorderRadius.reset(); - mBackgroundColorRenderPath.reset(); - mOuterClipPathForBorderRadius.reset(); - mPathForBorderRadiusOutline.reset(); - mCenterDrawPath.reset(); - - mInnerClipTempRectForBorderRadius.set(getBounds()); - mOuterClipTempRectForBorderRadius.set(getBounds()); - mTempRectForBorderRadiusOutline.set(getBounds()); - mTempRectForCenterDrawPath.set(getBounds()); - - final RectF borderWidth = getDirectionAwareBorderInsets(); - - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - int borderColor = getBorderColor(Spacing.ALL); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - // Clip border ONLY if its color is non transparent - if (Color.alpha(colorLeft) != 0 - && Color.alpha(colorTop) != 0 - && Color.alpha(colorRight) != 0 - && Color.alpha(colorBottom) != 0 - && Color.alpha(borderColor) != 0) { - - mInnerClipTempRectForBorderRadius.top += borderWidth.top; - mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; - mInnerClipTempRectForBorderRadius.left += borderWidth.left; - mInnerClipTempRectForBorderRadius.right -= borderWidth.right; - } - - mTempRectForCenterDrawPath.top += borderWidth.top * 0.5f; - mTempRectForCenterDrawPath.bottom -= borderWidth.bottom * 0.5f; - mTempRectForCenterDrawPath.left += borderWidth.left * 0.5f; - mTempRectForCenterDrawPath.right -= borderWidth.right * 0.5f; - - final float borderRadius = getFullBorderRadius(); - float topLeftRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_LEFT); - float topRightRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_RIGHT); - float bottomLeftRadius = - getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_LEFT); - float bottomRightRadius = - getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT); - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float topStartRadius = getBorderRadius(BorderRadiusLocation.TOP_START); - float topEndRadius = getBorderRadius(BorderRadiusLocation.TOP_END); - float bottomStartRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_START); - float bottomEndRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_END); - - float endEndRadius = getBorderRadius(BorderRadiusLocation.END_END); - float endStartRadius = getBorderRadius(BorderRadiusLocation.END_START); - float startEndRadius = getBorderRadius(BorderRadiusLocation.START_END); - float startStartRadius = getBorderRadius(BorderRadiusLocation.START_START); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (YogaConstants.isUndefined(topStartRadius)) { - topStartRadius = topLeftRadius; - } - - if (YogaConstants.isUndefined(topEndRadius)) { - topEndRadius = topRightRadius; - } - - if (YogaConstants.isUndefined(bottomStartRadius)) { - bottomStartRadius = bottomLeftRadius; - } - - if (YogaConstants.isUndefined(bottomEndRadius)) { - bottomEndRadius = bottomRightRadius; - } - - final float logicalTopStartRadius = - YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; - final float logicalTopEndRadius = - YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; - final float logicalBottomStartRadius = - YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; - final float logicalBottomEndRadius = - YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; - - final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; - final float directionAwareTopRightRadius = - isRTL ? logicalTopStartRadius : logicalTopEndRadius; - final float directionAwareBottomLeftRadius = - isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; - final float directionAwareBottomRightRadius = - isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; - - topLeftRadius = directionAwareTopLeftRadius; - topRightRadius = directionAwareTopRightRadius; - bottomLeftRadius = directionAwareBottomLeftRadius; - bottomRightRadius = directionAwareBottomRightRadius; - } else { - final float logicalTopStartRadius = - YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; - final float logicalTopEndRadius = - YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; - final float logicalBottomStartRadius = - YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; - final float logicalBottomEndRadius = - YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; - - final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; - final float directionAwareTopRightRadius = - isRTL ? logicalTopStartRadius : logicalTopEndRadius; - final float directionAwareBottomLeftRadius = - isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; - final float directionAwareBottomRightRadius = - isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; - - if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) { - topLeftRadius = directionAwareTopLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) { - topRightRadius = directionAwareTopRightRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) { - bottomLeftRadius = directionAwareBottomLeftRadius; - } - - if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) { - bottomRightRadius = directionAwareBottomRightRadius; - } - } - - final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderWidth.left, 0); - final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderWidth.top, 0); - final float innerTopRightRadiusX = Math.max(topRightRadius - borderWidth.right, 0); - final float innerTopRightRadiusY = Math.max(topRightRadius - borderWidth.top, 0); - final float innerBottomRightRadiusX = Math.max(bottomRightRadius - borderWidth.right, 0); - final float innerBottomRightRadiusY = Math.max(bottomRightRadius - borderWidth.bottom, 0); - final float innerBottomLeftRadiusX = Math.max(bottomLeftRadius - borderWidth.left, 0); - final float innerBottomLeftRadiusY = Math.max(bottomLeftRadius - borderWidth.bottom, 0); - - mInnerClipPathForBorderRadius.addRoundRect( - mInnerClipTempRectForBorderRadius, - new float[] { - innerTopLeftRadiusX, - innerTopLeftRadiusY, - innerTopRightRadiusX, - innerTopRightRadiusY, - innerBottomRightRadiusX, - innerBottomRightRadiusY, - innerBottomLeftRadiusX, - innerBottomLeftRadiusY, - }, - Path.Direction.CW); - - // There is a small gap between mBackgroundColorRenderPath and its - // border. mGapBetweenPaths is used to slightly enlarge the rectangle - // (mInnerClipTempRectForBorderRadius), ensuring the border can be - // drawn on top without the gap. - mBackgroundColorRenderPath.addRoundRect( - mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths, - mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths, - new float[] { - innerTopLeftRadiusX, - innerTopLeftRadiusY, - innerTopRightRadiusX, - innerTopRightRadiusY, - innerBottomRightRadiusX, - innerBottomRightRadiusY, - innerBottomLeftRadiusX, - innerBottomLeftRadiusY, - }, - Path.Direction.CW); - - mOuterClipPathForBorderRadius.addRoundRect( - mOuterClipTempRectForBorderRadius, - new float[] { - topLeftRadius, - topLeftRadius, - topRightRadius, - topRightRadius, - bottomRightRadius, - bottomRightRadius, - bottomLeftRadius, - bottomLeftRadius - }, - Path.Direction.CW); - - float extraRadiusForOutline = 0; - - if (mBorderWidth != null) { - extraRadiusForOutline = mBorderWidth.get(Spacing.ALL) / 2f; - } - - mPathForBorderRadiusOutline.addRoundRect( - mTempRectForBorderRadiusOutline, - new float[] { - topLeftRadius + extraRadiusForOutline, - topLeftRadius + extraRadiusForOutline, - topRightRadius + extraRadiusForOutline, - topRightRadius + extraRadiusForOutline, - bottomRightRadius + extraRadiusForOutline, - bottomRightRadius + extraRadiusForOutline, - bottomLeftRadius + extraRadiusForOutline, - bottomLeftRadius + extraRadiusForOutline - }, - Path.Direction.CW); - - mCenterDrawPath.addRoundRect( - mTempRectForCenterDrawPath, - new float[] { - Math.max( - topLeftRadius - borderWidth.left * 0.5f, - (borderWidth.left > 0.0f) ? (topLeftRadius / borderWidth.left) : 0.0f), - Math.max( - topLeftRadius - borderWidth.top * 0.5f, - (borderWidth.top > 0.0f) ? (topLeftRadius / borderWidth.top) : 0.0f), - Math.max( - topRightRadius - borderWidth.right * 0.5f, - (borderWidth.right > 0.0f) ? (topRightRadius / borderWidth.right) : 0.0f), - Math.max( - topRightRadius - borderWidth.top * 0.5f, - (borderWidth.top > 0.0f) ? (topRightRadius / borderWidth.top) : 0.0f), - Math.max( - bottomRightRadius - borderWidth.right * 0.5f, - (borderWidth.right > 0.0f) ? (bottomRightRadius / borderWidth.right) : 0.0f), - Math.max( - bottomRightRadius - borderWidth.bottom * 0.5f, - (borderWidth.bottom > 0.0f) ? (bottomRightRadius / borderWidth.bottom) : 0.0f), - Math.max( - bottomLeftRadius - borderWidth.left * 0.5f, - (borderWidth.left > 0.0f) ? (bottomLeftRadius / borderWidth.left) : 0.0f), - Math.max( - bottomLeftRadius - borderWidth.bottom * 0.5f, - (borderWidth.bottom > 0.0f) ? (bottomLeftRadius / borderWidth.bottom) : 0.0f) - }, - Path.Direction.CW); - - /** - * Rounded Multi-Colored Border Algorithm: - * - *

Let O (for outer) = (top, left, bottom, right) be the rectangle that represents the size - * and position of a view V. Since the box-sizing of all React Native views is border-box, any - * border of V will render inside O. - * - *

Let BorderWidth = (borderTop, borderLeft, borderBottom, borderRight). - * - *

Let I (for inner) = O - BorderWidth. - * - *

Then, remembering that O and I are rectangles and that I is inside O, O - I gives us the - * border of V. Therefore, we can use canvas.clipPath to draw V's border. - * - *

canvas.clipPath(O, Region.OP.INTERSECT); - * - *

canvas.clipPath(I, Region.OP.DIFFERENCE); - * - *

canvas.drawRect(O, paint); - * - *

This lets us draw non-rounded single-color borders. - * - *

To extend this algorithm to rounded single-color borders, we: - * - *

1. Curve the corners of O by the (border radii of V) using Path#addRoundRect. - * - *

2. Curve the corners of I by (border radii of V - border widths of V) using - * Path#addRoundRect. - * - *

Let O' = curve(O, border radii of V). - * - *

Let I' = curve(I, border radii of V - border widths of V) - * - *

The rationale behind this decision is the (first sentence of the) following section in the - * CSS Backgrounds and Borders Module Level 3: - * https://www.w3.org/TR/css3-background/#the-border-radius. - * - *

After both O and I have been curved, we can execute the following lines once again to - * render curved single-color borders: - * - *

canvas.clipPath(O, Region.OP.INTERSECT); - * - *

canvas.clipPath(I, Region.OP.DIFFERENCE); - * - *

canvas.drawRect(O, paint); - * - *

To extend this algorithm to rendering multi-colored rounded borders, we render each side - * of the border as its own quadrilateral. Suppose that we were handling the case where all the - * border radii are 0. Then, the four quadrilaterals would be: - * - *

Left: (O.left, O.top), (I.left, I.top), (I.left, I.bottom), (O.left, O.bottom) - * - *

Top: (O.left, O.top), (I.left, I.top), (I.right, I.top), (O.right, O.top) - * - *

Right: (O.right, O.top), (I.right, I.top), (I.right, I.bottom), (O.right, O.bottom) - * - *

Bottom: (O.right, O.bottom), (I.right, I.bottom), (I.left, I.bottom), (O.left, O.bottom) - * - *

Now, lets consider what happens when we render a rounded border (radii != 0). For the sake - * of simplicity, let's focus on the top edge of the Left border: - * - *

Let borderTopLeftRadius = 5. Let borderLeftWidth = 1. Let borderTopWidth = 2. - * - *

We know that O is curved by the ellipse E_O (a = 5, b = 5). We know that I is curved by - * the ellipse E_I (a = 5 - 1, b = 5 - 2). - * - *

Since we have clipping, it should be safe to set the top-left point of the Left - * quadrilateral's top edge to (O.left, O.top). - * - *

But, what should the top-right point be? - * - *

The fact that the border is curved shouldn't change the slope (nor the position) of the - * line connecting the top-left and top-right points of the Left quadrilateral's top edge. - * Therefore, The top-right point should lie somewhere on the line L = (1 - a) * (O.left, O.top) - * + a * (I.left, I.top). - * - *

a != 0, because then the top-left and top-right points would be the same and - * borderLeftWidth = 1. a != 1, because then the top-right point would not touch an edge of the - * ellipse E_I. We want the top-right point to touch an edge of the inner ellipse because the - * border curves with E_I on the top-left corner of V. - * - *

Therefore, it must be the case that a > 1. Two natural locations of the top-right point - * exist: 1. The first intersection of L with E_I. 2. The second intersection of L with E_I. - * - *

We choose the top-right point of the top edge of the Left quadrilateral to be an arbitrary - * intersection of L with E_I. - */ - if (mInnerTopLeftCorner == null) { - mInnerTopLeftCorner = new PointF(); - } - - /** Compute mInnerTopLeftCorner */ - mInnerTopLeftCorner.x = mInnerClipTempRectForBorderRadius.left; - mInnerTopLeftCorner.y = mInnerClipTempRectForBorderRadius.top; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.top, - mInnerClipTempRectForBorderRadius.left + 2 * innerTopLeftRadiusX, - mInnerClipTempRectForBorderRadius.top + 2 * innerTopLeftRadiusY, - - // Line Start - mOuterClipTempRectForBorderRadius.left, - mOuterClipTempRectForBorderRadius.top, - - // Line End - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.top, - - // Result - mInnerTopLeftCorner); - - /** Compute mInnerBottomLeftCorner */ - if (mInnerBottomLeftCorner == null) { - mInnerBottomLeftCorner = new PointF(); - } - - mInnerBottomLeftCorner.x = mInnerClipTempRectForBorderRadius.left; - mInnerBottomLeftCorner.y = mInnerClipTempRectForBorderRadius.bottom; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomLeftRadiusY, - mInnerClipTempRectForBorderRadius.left + 2 * innerBottomLeftRadiusX, - mInnerClipTempRectForBorderRadius.bottom, - - // Line Start - mOuterClipTempRectForBorderRadius.left, - mOuterClipTempRectForBorderRadius.bottom, - - // Line End - mInnerClipTempRectForBorderRadius.left, - mInnerClipTempRectForBorderRadius.bottom, - - // Result - mInnerBottomLeftCorner); - - /** Compute mInnerTopRightCorner */ - if (mInnerTopRightCorner == null) { - mInnerTopRightCorner = new PointF(); - } - - mInnerTopRightCorner.x = mInnerClipTempRectForBorderRadius.right; - mInnerTopRightCorner.y = mInnerClipTempRectForBorderRadius.top; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.right - 2 * innerTopRightRadiusX, - mInnerClipTempRectForBorderRadius.top, - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.top + 2 * innerTopRightRadiusY, - - // Line Start - mOuterClipTempRectForBorderRadius.right, - mOuterClipTempRectForBorderRadius.top, - - // Line End - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.top, - - // Result - mInnerTopRightCorner); - - /** Compute mInnerBottomRightCorner */ - if (mInnerBottomRightCorner == null) { - mInnerBottomRightCorner = new PointF(); - } - - mInnerBottomRightCorner.x = mInnerClipTempRectForBorderRadius.right; - mInnerBottomRightCorner.y = mInnerClipTempRectForBorderRadius.bottom; - - getEllipseIntersectionWithLine( - // Ellipse Bounds - mInnerClipTempRectForBorderRadius.right - 2 * innerBottomRightRadiusX, - mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomRightRadiusY, - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.bottom, - - // Line Start - mOuterClipTempRectForBorderRadius.right, - mOuterClipTempRectForBorderRadius.bottom, - - // Line End - mInnerClipTempRectForBorderRadius.right, - mInnerClipTempRectForBorderRadius.bottom, - - // Result - mInnerBottomRightCorner); - } - - private static void getEllipseIntersectionWithLine( - double ellipseBoundsLeft, - double ellipseBoundsTop, - double ellipseBoundsRight, - double ellipseBoundsBottom, - double lineStartX, - double lineStartY, - double lineEndX, - double lineEndY, - PointF result) { - final double ellipseCenterX = (ellipseBoundsLeft + ellipseBoundsRight) / 2; - final double ellipseCenterY = (ellipseBoundsTop + ellipseBoundsBottom) / 2; - - /** - * Step 1: - * - *

Translate the line so that the ellipse is at the origin. - * - *

Why? It makes the math easier by changing the ellipse equation from ((x - - * ellipseCenterX)/a)^2 + ((y - ellipseCenterY)/b)^2 = 1 to (x/a)^2 + (y/b)^2 = 1. - */ - lineStartX -= ellipseCenterX; - lineStartY -= ellipseCenterY; - lineEndX -= ellipseCenterX; - lineEndY -= ellipseCenterY; - - /** - * Step 2: - * - *

Ellipse equation: (x/a)^2 + (y/b)^2 = 1 Line equation: y = mx + c - */ - final double a = Math.abs(ellipseBoundsRight - ellipseBoundsLeft) / 2; - final double b = Math.abs(ellipseBoundsBottom - ellipseBoundsTop) / 2; - final double m = (lineEndY - lineStartY) / (lineEndX - lineStartX); - final double c = lineStartY - m * lineStartX; // Just a point on the line - - /** - * Step 3: - * - *

Substitute the Line equation into the Ellipse equation. Solve for x. Eventually, you'll - * have to use the quadratic formula. - * - *

Quadratic formula: Ax^2 + Bx + C = 0 - */ - final double A = (b * b + a * a * m * m); - final double B = 2 * a * a * c * m; - final double C = (a * a * (c * c - b * b)); - - /** - * Step 4: - * - *

Apply Quadratic formula. D = determinant / 2A - */ - final double D = Math.sqrt(-C / A + Math.pow(B / (2 * A), 2)); - final double x2 = -B / (2 * A) - D; - final double y2 = m * x2 + c; - - /** - * Step 5: - * - *

Undo the space transformation in Step 5. - */ - final double x = x2 + ellipseCenterX; - final double y = y2 + ellipseCenterY; - - if (!Double.isNaN(x) && !Double.isNaN(y)) { - result.x = (float) x; - result.y = (float) y; - } - } - - public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { - if (mBorderWidth == null) { - return defaultValue; - } - - final float width = mBorderWidth.getRaw(spacingType); - - if (YogaConstants.isUndefined(width)) { - return defaultValue; - } - - return width; - } - - /** Set type of border */ - private void updatePathEffect() { - // Used for rounded border and rounded background - PathEffect mPathEffectForBorderStyle = - mBorderStyle != null ? BorderStyle.getPathEffect(mBorderStyle, getFullBorderWidth()) : null; - - mPaint.setPathEffect(mPathEffectForBorderStyle); - } - - private void updatePathEffect(int borderWidth) { - PathEffect pathEffectForBorderStyle = null; - if (mBorderStyle != null) { - pathEffectForBorderStyle = BorderStyle.getPathEffect(mBorderStyle, borderWidth); - } - mPaint.setPathEffect(pathEffectForBorderStyle); - } - - /** For rounded borders we use default "borderWidth" property. */ - public float getFullBorderWidth() { - return (mBorderWidth != null && !YogaConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) - ? mBorderWidth.getRaw(Spacing.ALL) - : 0f; - } - - /** - * Quickly determine if all the set border colors are equal. Bitwise AND all the set colors - * together, then OR them all together. If the AND and the OR are the same, then the colors are - * compatible, so return this color. - * - *

Used to avoid expensive path creation and expensive calls to canvas.drawPath - * - * @return A compatible border color, or zero if the border colors are not compatible. - */ - private static int fastBorderCompatibleColorOrZero( - int borderLeft, - int borderTop, - int borderRight, - int borderBottom, - int colorLeft, - int colorTop, - int colorRight, - int colorBottom) { - int andSmear = - (borderLeft > 0 ? colorLeft : ALL_BITS_SET) - & (borderTop > 0 ? colorTop : ALL_BITS_SET) - & (borderRight > 0 ? colorRight : ALL_BITS_SET) - & (borderBottom > 0 ? colorBottom : ALL_BITS_SET); - int orSmear = - (borderLeft > 0 ? colorLeft : ALL_BITS_UNSET) - | (borderTop > 0 ? colorTop : ALL_BITS_UNSET) - | (borderRight > 0 ? colorRight : ALL_BITS_UNSET) - | (borderBottom > 0 ? colorBottom : ALL_BITS_UNSET); - return andSmear == orSmear ? andSmear : 0; - } - - private void drawRectangularBackgroundWithBorders(Canvas canvas) { - mPaint.setStyle(Paint.Style.FILL); - - int useColor = ColorUtil.multiplyColorAlpha(mColor, mAlpha); - if (Color.alpha(useColor) != 0) { // color is not transparent - mPaint.setColor(useColor); - canvas.drawRect(getBounds(), mPaint); - } - - final RectF borderWidth = getDirectionAwareBorderInsets(); - - final int borderLeft = Math.round(borderWidth.left); - final int borderTop = Math.round(borderWidth.top); - final int borderRight = Math.round(borderWidth.right); - final int borderBottom = Math.round(borderWidth.bottom); - - // maybe draw borders? - if (borderLeft > 0 || borderRight > 0 || borderTop > 0 || borderBottom > 0) { - Rect bounds = getBounds(); - - int colorLeft = getBorderColor(Spacing.LEFT); - int colorTop = getBorderColor(Spacing.TOP); - int colorRight = getBorderColor(Spacing.RIGHT); - int colorBottom = getBorderColor(Spacing.BOTTOM); - - int colorBlock = getBorderColor(Spacing.BLOCK); - int colorBlockStart = getBorderColor(Spacing.BLOCK_START); - int colorBlockEnd = getBorderColor(Spacing.BLOCK_END); - - if (isBorderColorDefined(Spacing.BLOCK)) { - colorBottom = colorBlock; - colorTop = colorBlock; - } - if (isBorderColorDefined(Spacing.BLOCK_END)) { - colorBottom = colorBlockEnd; - } - if (isBorderColorDefined(Spacing.BLOCK_START)) { - colorTop = colorBlockStart; - } - - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - int colorStart = getBorderColor(Spacing.START); - int colorEnd = getBorderColor(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (!isBorderColorDefined(Spacing.START)) { - colorStart = colorLeft; - } - - if (!isBorderColorDefined(Spacing.END)) { - colorEnd = colorRight; - } - - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - colorLeft = directionAwareColorLeft; - colorRight = directionAwareColorRight; - } else { - final int directionAwareColorLeft = isRTL ? colorEnd : colorStart; - final int directionAwareColorRight = isRTL ? colorStart : colorEnd; - - final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); - final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); - final boolean isDirectionAwareColorLeftDefined = - isRTL ? isColorEndDefined : isColorStartDefined; - final boolean isDirectionAwareColorRightDefined = - isRTL ? isColorStartDefined : isColorEndDefined; - - if (isDirectionAwareColorLeftDefined) { - colorLeft = directionAwareColorLeft; - } - - if (isDirectionAwareColorRightDefined) { - colorRight = directionAwareColorRight; - } - } - - int left = bounds.left; - int top = bounds.top; - - // Check for fast path to border drawing. - int fastBorderColor = - fastBorderCompatibleColorOrZero( - borderLeft, - borderTop, - borderRight, - borderBottom, - colorLeft, - colorTop, - colorRight, - colorBottom); - - if (fastBorderColor != 0) { - if (Color.alpha(fastBorderColor) != 0) { - // Border color is not transparent. - int right = bounds.right; - int bottom = bounds.bottom; - - mPaint.setColor(fastBorderColor); - mPaint.setStyle(Paint.Style.STROKE); - if (borderLeft > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.left); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left + width / 2, top); - mPathForSingleBorder.lineTo(left + width / 2, bottom); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderTop > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.top); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left, top + width / 2); - mPathForSingleBorder.lineTo(right, top + width / 2); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderRight > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.right); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(right - width / 2, top); - mPathForSingleBorder.lineTo(right - width / 2, bottom); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - if (borderBottom > 0) { - mPathForSingleBorder.reset(); - int width = Math.round(borderWidth.bottom); - updatePathEffect(width); - mPaint.setStrokeWidth(width); - mPathForSingleBorder.moveTo(left, bottom - width / 2); - mPathForSingleBorder.lineTo(right, bottom - width / 2); - canvas.drawPath(mPathForSingleBorder, mPaint); - } - } - } else { - // If the path drawn previously is of the same color, - // there would be a slight white space between borders - // with anti-alias set to true. - // Therefore we need to disable anti-alias, and - // after drawing is done, we will re-enable it. - - mPaint.setAntiAlias(false); - - int width = bounds.width(); - int height = bounds.height(); - - if (borderLeft > 0) { - final float x1 = left; - final float y1 = top; - final float x2 = left + borderLeft; - final float y2 = top + borderTop; - final float x3 = left + borderLeft; - final float y3 = top + height - borderBottom; - final float x4 = left; - final float y4 = top + height; - - drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderTop > 0) { - final float x1 = left; - final float y1 = top; - final float x2 = left + borderLeft; - final float y2 = top + borderTop; - final float x3 = left + width - borderRight; - final float y3 = top + borderTop; - final float x4 = left + width; - final float y4 = top; - - drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderRight > 0) { - final float x1 = left + width; - final float y1 = top; - final float x2 = left + width; - final float y2 = top + height; - final float x3 = left + width - borderRight; - final float y3 = top + height - borderBottom; - final float x4 = left + width - borderRight; - final float y4 = top + borderTop; - - drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); - } - - if (borderBottom > 0) { - final float x1 = left; - final float y1 = top + height; - final float x2 = left + width; - final float y2 = top + height; - final float x3 = left + width - borderRight; - final float y3 = top + height - borderBottom; - final float x4 = left + borderLeft; - final float y4 = top + height - borderBottom; - - drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); - } - - // re-enable anti alias - mPaint.setAntiAlias(true); - } - } - } - - private void drawQuadrilateral( - Canvas canvas, - int fillColor, - float x1, - float y1, - float x2, - float y2, - float x3, - float y3, - float x4, - float y4) { - if (fillColor == Color.TRANSPARENT) { - return; - } - - if (mPathForBorder == null) { - mPathForBorder = new Path(); - } - - mPaint.setColor(fillColor); - mPathForBorder.reset(); - mPathForBorder.moveTo(x1, y1); - mPathForBorder.lineTo(x2, y2); - mPathForBorder.lineTo(x3, y3); - mPathForBorder.lineTo(x4, y4); - mPathForBorder.lineTo(x1, y1); - canvas.drawPath(mPathForBorder, mPaint); - } - - private int getBorderWidth(int position) { - if (mBorderWidth == null) { - return 0; - } - - final float width = mBorderWidth.get(position); - return YogaConstants.isUndefined(width) ? -1 : Math.round(width); - } - - private static int colorFromAlphaAndRGBComponents(float alpha, float rgb) { - int rgbComponent = 0x00FFFFFF & (int) rgb; - int alphaComponent = 0xFF000000 & ((int) alpha) << 24; - - return rgbComponent | alphaComponent; - } - - private boolean isBorderColorDefined(int position) { - final float rgb = mBorderRGB != null ? mBorderRGB.get(position) : YogaConstants.UNDEFINED; - final float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : YogaConstants.UNDEFINED; - return !YogaConstants.isUndefined(rgb) && !YogaConstants.isUndefined(alpha); - } - - public int getBorderColor(int position) { - float rgb = mBorderRGB != null ? mBorderRGB.get(position) : DEFAULT_BORDER_RGB; - float alpha = mBorderAlpha != null ? mBorderAlpha.get(position) : DEFAULT_BORDER_ALPHA; - - return ReactViewBackgroundDrawable.colorFromAlphaAndRGBComponents(alpha, rgb); - } - - public RectF getDirectionAwareBorderInsets() { - final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); - final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); - final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); - float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); - float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); - - if (mBorderWidth != null) { - final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - float borderStartWidth = mBorderWidth.getRaw(Spacing.START); - float borderEndWidth = mBorderWidth.getRaw(Spacing.END); - - if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { - if (YogaConstants.isUndefined(borderStartWidth)) { - borderStartWidth = borderLeftWidth; - } - - if (YogaConstants.isUndefined(borderEndWidth)) { - borderEndWidth = borderRightWidth; - } - - final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; - final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; - - borderLeftWidth = directionAwareBorderLeftWidth; - borderRightWidth = directionAwareBorderRightWidth; - } else { - final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; - final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; - - if (!YogaConstants.isUndefined(directionAwareBorderLeftWidth)) { - borderLeftWidth = directionAwareBorderLeftWidth; - } - - if (!YogaConstants.isUndefined(directionAwareBorderRightWidth)) { - borderRightWidth = directionAwareBorderRightWidth; - } - } - } - - return new RectF(borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth); - } -} + package com.facebook.react.views.view; + + import android.content.Context; + import android.graphics.Canvas; + import android.graphics.Color; + import android.graphics.ColorSpace; + import android.graphics.ColorFilter; + import android.graphics.DashPathEffect; + import android.graphics.Outline; + import android.graphics.Paint; + import android.graphics.Path; + import android.graphics.PathEffect; + import android.graphics.PixelFormat; + import android.graphics.PointF; + import android.graphics.Rect; + import android.graphics.RectF; + import android.graphics.Region; + import android.graphics.drawable.Drawable; + import android.view.View; + import androidx.annotation.ColorLong; + import androidx.annotation.Nullable; + import com.facebook.react.common.annotations.VisibleForTesting; + import com.facebook.react.modules.i18nmanager.I18nUtil; + import com.facebook.react.uimanager.BorderColor; + import com.facebook.react.uimanager.FloatUtil; + import com.facebook.react.uimanager.Spacing; + import com.facebook.yoga.YogaConstants; + import java.util.Arrays; + import java.util.Locale; + + /** + * A subclass of {@link Drawable} used for background of {@link ReactViewGroup}. It supports drawing + * background color and borders (including rounded borders) by providing a react friendly API + * (setter for each of those properties). + * + *

The implementation tries to allocate as few objects as possible depending on which properties + * are set. E.g. for views with rounded background/borders we allocate {@code + * mInnerClipPathForBorderRadius} and {@code mInnerClipTempRectForBorderRadius}. In case when view + * have a rectangular borders we allocate {@code mBorderWidthResult} and similar. When only + * background color is set we won't allocate any extra/unnecessary objects. + */ + public class ReactViewBackgroundDrawable extends Drawable { + + private static final long DEFAULT_BORDER_COLOR = Color.pack(Color.BLACK); + // ~0 == 0xFFFFFFFF, all bits set to 1. + private static final int ALL_BITS_SET = ~0; + // 0 == 0x00000000, all bits set to 0. + private static final int ALL_BITS_UNSET = 0; + + private enum BorderStyle { + SOLID, + DASHED, + DOTTED; + + public static @Nullable PathEffect getPathEffect(BorderStyle style, float borderWidth) { + switch (style) { + case SOLID: + return null; + + case DASHED: + return new DashPathEffect( + new float[] {borderWidth * 3, borderWidth * 3, borderWidth * 3, borderWidth * 3}, 0); + + case DOTTED: + return new DashPathEffect( + new float[] {borderWidth, borderWidth, borderWidth, borderWidth}, 0); + + default: + return null; + } + } + }; + + /* Value at Spacing.ALL index used for rounded borders, whole array used by rectangular borders */ + private @Nullable Spacing mBorderWidth; + private @Nullable BorderColor mBorderColor; + private @Nullable BorderStyle mBorderStyle; + + private @Nullable Path mInnerClipPathForBorderRadius; + private @Nullable Path mBackgroundColorRenderPath; + private @Nullable Path mOuterClipPathForBorderRadius; + private @Nullable Path mPathForBorderRadiusOutline; + private @Nullable Path mPathForBorder; + private final Path mPathForSingleBorder = new Path(); + private @Nullable Path mCenterDrawPath; + private @Nullable RectF mInnerClipTempRectForBorderRadius; + private @Nullable RectF mOuterClipTempRectForBorderRadius; + private @Nullable RectF mTempRectForBorderRadiusOutline; + private @Nullable RectF mTempRectForCenterDrawPath; + private @Nullable PointF mInnerTopLeftCorner; + private @Nullable PointF mInnerTopRightCorner; + private @Nullable PointF mInnerBottomRightCorner; + private @Nullable PointF mInnerBottomLeftCorner; + private boolean mNeedUpdatePathForBorderRadius = false; + private float mBorderRadius = YogaConstants.UNDEFINED; + + /* Used by all types of background and for drawing borders */ + private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private long mColor = Color.pack(Color.TRANSPARENT); + private int mAlpha = 255; + + // There is a small gap between the edges of adjacent paths + // such as between the mBackgroundColorRenderPath and its border. + // The smallest amount (found to be 0.8f) is used to extend + // the paths, overlapping them and closing the visible gap. + private final float mGapBetweenPaths = 0.8f; + + private @Nullable float[] mBorderCornerRadii; + private final Context mContext; + private int mLayoutDirection; + + public enum BorderRadiusLocation { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_RIGHT, + BOTTOM_LEFT, + TOP_START, + TOP_END, + BOTTOM_START, + BOTTOM_END, + END_END, + END_START, + START_END, + START_START + } + + public ReactViewBackgroundDrawable(Context context) { + mContext = context; + } + + @Override + public void draw(Canvas canvas) { + updatePathEffect(); + if (!hasRoundedBorders()) { + drawRectangularBackgroundWithBorders(canvas); + } else { + drawRoundedBackgroundWithBorders(canvas); + } + } + + public boolean hasRoundedBorders() { + if (!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) { + return true; + } + + if (mBorderCornerRadii != null) { + for (final float borderRadii : mBorderCornerRadii) { + if (!YogaConstants.isUndefined(borderRadii) && borderRadii > 0) { + return true; + } + } + } + + return false; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + mNeedUpdatePathForBorderRadius = true; + } + + @Override + public void setAlpha(int alpha) { + if (alpha != mAlpha) { + mAlpha = alpha; + invalidateSelf(); + } + } + + @Override + public int getAlpha() { + return mAlpha; + } + + public long useColor() { + Color color = Color.valueOf(mColor); + float colorAlpha = color.alpha(); + float combinedAlpha = colorAlpha * (mAlpha / 255.0f); + return Color.pack(color.red(), color.green(), color.blue(), combinedAlpha, color.getColorSpace()); + } + + @Override + public void setColorFilter(ColorFilter cf) { + // do nothing + } + + @Override + public int getOpacity() { + float alpha = Color.valueOf(mColor).alpha() * (mAlpha / 255.0f); + if (alpha == 1.0f) { + return PixelFormat.OPAQUE; + } else if (alpha == 0.0f) { + return PixelFormat.TRANSPARENT; + } else { + return PixelFormat.TRANSLUCENT; + } + } + + /* Android's elevation implementation requires this to be implemented to know where to draw the shadow. */ + @Override + public void getOutline(Outline outline) { + if ((!YogaConstants.isUndefined(mBorderRadius) && mBorderRadius > 0) + || mBorderCornerRadii != null) { + updatePath(); + + outline.setConvexPath(mPathForBorderRadiusOutline); + } else { + outline.setRect(getBounds()); + } + } + + public void setBorderWidth(int position, float width) { + if (mBorderWidth == null) { + mBorderWidth = new Spacing(YogaConstants.UNDEFINED); + } + if (!FloatUtil.floatsEqual(mBorderWidth.getRaw(position), width)) { + mBorderWidth.set(position, width); + switch (position) { + case Spacing.ALL: + case Spacing.LEFT: + case Spacing.BOTTOM: + case Spacing.RIGHT: + case Spacing.TOP: + case Spacing.START: + case Spacing.END: + mNeedUpdatePathForBorderRadius = true; + } + invalidateSelf(); + } + } + + public void setBorderColor(int position, long color) { + if (mBorderColor == null) { + mBorderColor = new BorderColor(DEFAULT_BORDER_COLOR); + } + if (mBorderColor.getRaw(position) != color) { + mBorderColor.set(position, color); + invalidateSelf(); + } + mNeedUpdatePathForBorderRadius = true; + } + + public void setBorderStyle(@Nullable String style) { + BorderStyle borderStyle = + style == null ? null : BorderStyle.valueOf(style.toUpperCase(Locale.US)); + if (mBorderStyle != borderStyle) { + mBorderStyle = borderStyle; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setRadius(float radius) { + if (!FloatUtil.floatsEqual(mBorderRadius, radius)) { + mBorderRadius = radius; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public void setRadius(float radius, int position) { + if (mBorderCornerRadii == null) { + mBorderCornerRadii = new float[12]; + Arrays.fill(mBorderCornerRadii, YogaConstants.UNDEFINED); + } + + if (!FloatUtil.floatsEqual(mBorderCornerRadii[position], radius)) { + mBorderCornerRadii[position] = radius; + mNeedUpdatePathForBorderRadius = true; + invalidateSelf(); + } + } + + public float getFullBorderRadius() { + return YogaConstants.isUndefined(mBorderRadius) ? 0 : mBorderRadius; + } + + public float getBorderRadius(final BorderRadiusLocation location) { + return getBorderRadiusOrDefaultTo(YogaConstants.UNDEFINED, location); + } + + public float getBorderRadiusOrDefaultTo( + final float defaultValue, final BorderRadiusLocation location) { + if (mBorderCornerRadii == null) { + return defaultValue; + } + + final float radius = mBorderCornerRadii[location.ordinal()]; + + if (YogaConstants.isUndefined(radius)) { + return defaultValue; + } + + return radius; + } + + public void setColor(int color) { + mColor = Color.pack(color); + invalidateSelf(); + } + + public void setColor(long color) { + mColor = color; + invalidateSelf(); + } + + /** Similar to Drawable.getLayoutDirection, but available in APIs < 23. */ + public int getResolvedLayoutDirection() { + return mLayoutDirection; + } + + /** Similar to Drawable.setLayoutDirection, but available in APIs < 23. */ + public boolean setResolvedLayoutDirection(int layoutDirection) { + if (mLayoutDirection != layoutDirection) { + mLayoutDirection = layoutDirection; + return onResolvedLayoutDirectionChanged(layoutDirection); + } + return false; + } + + /** Similar to Drawable.onLayoutDirectionChanged, but available in APIs < 23. */ + public boolean onResolvedLayoutDirectionChanged(int layoutDirection) { + return false; + } + + @VisibleForTesting + public int getColor() { + return Color.toArgb(mColor); + } + + private void drawRoundedBackgroundWithBorders(Canvas canvas) { + updatePath(); + canvas.save(); + + // Clip outer border + canvas.clipPath(mOuterClipPathForBorderRadius, Region.Op.INTERSECT); + + // Draws the View without its border first (with background color fill) + long color = useColor(); + if (Color.alpha(color) != 0.0f) { // color is not transparent + mPaint.setColor(color); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(mBackgroundColorRenderPath, mPaint); + } + + final RectF borderWidth = getDirectionAwareBorderInsets(); + long colorLeft = getBorderColor(Spacing.LEFT); + long colorTop = getBorderColor(Spacing.TOP); + long colorRight = getBorderColor(Spacing.RIGHT); + long colorBottom = getBorderColor(Spacing.BOTTOM); + + long colorBlock = getBorderColor(Spacing.BLOCK); + long colorBlockStart = getBorderColor(Spacing.BLOCK_START); + long colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + if (borderWidth.top > 0 + || borderWidth.bottom > 0 + || borderWidth.left > 0 + || borderWidth.right > 0) { + + // If it's a full and even border draw inner rect path with stroke + final float fullBorderWidth = getFullBorderWidth(); + long borderColor = getBorderColor(Spacing.ALL); + if (borderWidth.top == fullBorderWidth + && borderWidth.bottom == fullBorderWidth + && borderWidth.left == fullBorderWidth + && borderWidth.right == fullBorderWidth + && colorLeft == borderColor + && colorTop == borderColor + && colorRight == borderColor + && colorBottom == borderColor) { + if (fullBorderWidth > 0) { + long alphaAdjustedBorderColor = + Color.pack( + Color.red(borderColor), + Color.green(borderColor), + Color.blue(borderColor), + Color.alpha(borderColor) * (mAlpha / 255.0f), + Color.colorSpace(borderColor)); + mPaint.setColor(alphaAdjustedBorderColor); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(fullBorderWidth); + canvas.drawPath(mCenterDrawPath, mPaint); + } + } + // In the case of uneven border widths/colors draw quadrilateral in each direction + else { + mPaint.setStyle(Paint.Style.FILL); + + // Clip inner border + canvas.clipPath(mInnerClipPathForBorderRadius, Region.Op.DIFFERENCE); + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + long colorStart = getBorderColor(Spacing.START); + long colorEnd = getBorderColor(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (!isBorderColorDefined(Spacing.START)) { + colorStart = colorLeft; + } + + if (!isBorderColorDefined(Spacing.END)) { + colorEnd = colorRight; + } + + final long directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final long directionAwareColorRight = isRTL ? colorStart : colorEnd; + + colorLeft = directionAwareColorLeft; + colorRight = directionAwareColorRight; + } else { + final long directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final long directionAwareColorRight = isRTL ? colorStart : colorEnd; + + final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); + final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); + final boolean isDirectionAwareColorLeftDefined = + isRTL ? isColorEndDefined : isColorStartDefined; + final boolean isDirectionAwareColorRightDefined = + isRTL ? isColorStartDefined : isColorEndDefined; + + if (isDirectionAwareColorLeftDefined) { + colorLeft = directionAwareColorLeft; + } + + if (isDirectionAwareColorRightDefined) { + colorRight = directionAwareColorRight; + } + } + + final float left = mOuterClipTempRectForBorderRadius.left; + final float right = mOuterClipTempRectForBorderRadius.right; + final float top = mOuterClipTempRectForBorderRadius.top; + final float bottom = mOuterClipTempRectForBorderRadius.bottom; + + // mGapBetweenPaths is used to close the gap between the diagonal + // edges of the quadrilaterals on adjacent sides of the rectangle + if (borderWidth.left > 0) { + final float x1 = left; + final float y1 = top - mGapBetweenPaths; + final float x2 = mInnerTopLeftCorner.x; + final float y2 = mInnerTopLeftCorner.y - mGapBetweenPaths; + final float x3 = mInnerBottomLeftCorner.x; + final float y3 = mInnerBottomLeftCorner.y + mGapBetweenPaths; + final float x4 = left; + final float y4 = bottom + mGapBetweenPaths; + + drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.top > 0) { + final float x1 = left - mGapBetweenPaths; + final float y1 = top; + final float x2 = mInnerTopLeftCorner.x - mGapBetweenPaths; + final float y2 = mInnerTopLeftCorner.y; + final float x3 = mInnerTopRightCorner.x + mGapBetweenPaths; + final float y3 = mInnerTopRightCorner.y; + final float x4 = right + mGapBetweenPaths; + final float y4 = top; + + drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.right > 0) { + final float x1 = right; + final float y1 = top - mGapBetweenPaths; + final float x2 = mInnerTopRightCorner.x; + final float y2 = mInnerTopRightCorner.y - mGapBetweenPaths; + final float x3 = mInnerBottomRightCorner.x; + final float y3 = mInnerBottomRightCorner.y + mGapBetweenPaths; + final float x4 = right; + final float y4 = bottom + mGapBetweenPaths; + + drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderWidth.bottom > 0) { + final float x1 = left - mGapBetweenPaths; + final float y1 = bottom; + final float x2 = mInnerBottomLeftCorner.x - mGapBetweenPaths; + final float y2 = mInnerBottomLeftCorner.y; + final float x3 = mInnerBottomRightCorner.x + mGapBetweenPaths; + final float y3 = mInnerBottomRightCorner.y; + final float x4 = right + mGapBetweenPaths; + final float y4 = bottom; + + drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); + } + } + } + + canvas.restore(); + } + + private void updatePath() { + if (!mNeedUpdatePathForBorderRadius) { + return; + } + + mNeedUpdatePathForBorderRadius = false; + + if (mInnerClipPathForBorderRadius == null) { + mInnerClipPathForBorderRadius = new Path(); + } + + if (mBackgroundColorRenderPath == null) { + mBackgroundColorRenderPath = new Path(); + } + + if (mOuterClipPathForBorderRadius == null) { + mOuterClipPathForBorderRadius = new Path(); + } + + if (mPathForBorderRadiusOutline == null) { + mPathForBorderRadiusOutline = new Path(); + } + + if (mCenterDrawPath == null) { + mCenterDrawPath = new Path(); + } + + if (mInnerClipTempRectForBorderRadius == null) { + mInnerClipTempRectForBorderRadius = new RectF(); + } + + if (mOuterClipTempRectForBorderRadius == null) { + mOuterClipTempRectForBorderRadius = new RectF(); + } + + if (mTempRectForBorderRadiusOutline == null) { + mTempRectForBorderRadiusOutline = new RectF(); + } + + if (mTempRectForCenterDrawPath == null) { + mTempRectForCenterDrawPath = new RectF(); + } + + mInnerClipPathForBorderRadius.reset(); + mBackgroundColorRenderPath.reset(); + mOuterClipPathForBorderRadius.reset(); + mPathForBorderRadiusOutline.reset(); + mCenterDrawPath.reset(); + + mInnerClipTempRectForBorderRadius.set(getBounds()); + mOuterClipTempRectForBorderRadius.set(getBounds()); + mTempRectForBorderRadiusOutline.set(getBounds()); + mTempRectForCenterDrawPath.set(getBounds()); + + final RectF borderWidth = getDirectionAwareBorderInsets(); + + long colorLeft = getBorderColor(Spacing.LEFT); + long colorTop = getBorderColor(Spacing.TOP); + long colorRight = getBorderColor(Spacing.RIGHT); + long colorBottom = getBorderColor(Spacing.BOTTOM); + long borderColor = getBorderColor(Spacing.ALL); + + long colorBlock = getBorderColor(Spacing.BLOCK); + long colorBlockStart = getBorderColor(Spacing.BLOCK_START); + long colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + // Clip border ONLY if its color is non transparent + if (Color.alpha(colorLeft) != 0 + && Color.alpha(colorTop) != 0 + && Color.alpha(colorRight) != 0 + && Color.alpha(colorBottom) != 0 + && Color.alpha(borderColor) != 0) { + + mInnerClipTempRectForBorderRadius.top += borderWidth.top; + mInnerClipTempRectForBorderRadius.bottom -= borderWidth.bottom; + mInnerClipTempRectForBorderRadius.left += borderWidth.left; + mInnerClipTempRectForBorderRadius.right -= borderWidth.right; + } + + mTempRectForCenterDrawPath.top += borderWidth.top * 0.5f; + mTempRectForCenterDrawPath.bottom -= borderWidth.bottom * 0.5f; + mTempRectForCenterDrawPath.left += borderWidth.left * 0.5f; + mTempRectForCenterDrawPath.right -= borderWidth.right * 0.5f; + + final float borderRadius = getFullBorderRadius(); + float topLeftRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_LEFT); + float topRightRadius = getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.TOP_RIGHT); + float bottomLeftRadius = + getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_LEFT); + float bottomRightRadius = + getBorderRadiusOrDefaultTo(borderRadius, BorderRadiusLocation.BOTTOM_RIGHT); + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + float topStartRadius = getBorderRadius(BorderRadiusLocation.TOP_START); + float topEndRadius = getBorderRadius(BorderRadiusLocation.TOP_END); + float bottomStartRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_START); + float bottomEndRadius = getBorderRadius(BorderRadiusLocation.BOTTOM_END); + + float endEndRadius = getBorderRadius(BorderRadiusLocation.END_END); + float endStartRadius = getBorderRadius(BorderRadiusLocation.END_START); + float startEndRadius = getBorderRadius(BorderRadiusLocation.START_END); + float startStartRadius = getBorderRadius(BorderRadiusLocation.START_START); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (YogaConstants.isUndefined(topStartRadius)) { + topStartRadius = topLeftRadius; + } + + if (YogaConstants.isUndefined(topEndRadius)) { + topEndRadius = topRightRadius; + } + + if (YogaConstants.isUndefined(bottomStartRadius)) { + bottomStartRadius = bottomLeftRadius; + } + + if (YogaConstants.isUndefined(bottomEndRadius)) { + bottomEndRadius = bottomRightRadius; + } + + final float logicalTopStartRadius = + YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; + final float logicalTopEndRadius = + YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; + final float logicalBottomStartRadius = + YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; + final float logicalBottomEndRadius = + YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; + + final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; + final float directionAwareTopRightRadius = + isRTL ? logicalTopStartRadius : logicalTopEndRadius; + final float directionAwareBottomLeftRadius = + isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; + final float directionAwareBottomRightRadius = + isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; + + topLeftRadius = directionAwareTopLeftRadius; + topRightRadius = directionAwareTopRightRadius; + bottomLeftRadius = directionAwareBottomLeftRadius; + bottomRightRadius = directionAwareBottomRightRadius; + } else { + final float logicalTopStartRadius = + YogaConstants.isUndefined(topStartRadius) ? startStartRadius : topStartRadius; + final float logicalTopEndRadius = + YogaConstants.isUndefined(topEndRadius) ? startEndRadius : topEndRadius; + final float logicalBottomStartRadius = + YogaConstants.isUndefined(bottomStartRadius) ? endStartRadius : bottomStartRadius; + final float logicalBottomEndRadius = + YogaConstants.isUndefined(bottomEndRadius) ? endEndRadius : bottomEndRadius; + + final float directionAwareTopLeftRadius = isRTL ? logicalTopEndRadius : logicalTopStartRadius; + final float directionAwareTopRightRadius = + isRTL ? logicalTopStartRadius : logicalTopEndRadius; + final float directionAwareBottomLeftRadius = + isRTL ? logicalBottomEndRadius : logicalBottomStartRadius; + final float directionAwareBottomRightRadius = + isRTL ? logicalBottomStartRadius : logicalBottomEndRadius; + + if (!YogaConstants.isUndefined(directionAwareTopLeftRadius)) { + topLeftRadius = directionAwareTopLeftRadius; + } + + if (!YogaConstants.isUndefined(directionAwareTopRightRadius)) { + topRightRadius = directionAwareTopRightRadius; + } + + if (!YogaConstants.isUndefined(directionAwareBottomLeftRadius)) { + bottomLeftRadius = directionAwareBottomLeftRadius; + } + + if (!YogaConstants.isUndefined(directionAwareBottomRightRadius)) { + bottomRightRadius = directionAwareBottomRightRadius; + } + } + + final float innerTopLeftRadiusX = Math.max(topLeftRadius - borderWidth.left, 0); + final float innerTopLeftRadiusY = Math.max(topLeftRadius - borderWidth.top, 0); + final float innerTopRightRadiusX = Math.max(topRightRadius - borderWidth.right, 0); + final float innerTopRightRadiusY = Math.max(topRightRadius - borderWidth.top, 0); + final float innerBottomRightRadiusX = Math.max(bottomRightRadius - borderWidth.right, 0); + final float innerBottomRightRadiusY = Math.max(bottomRightRadius - borderWidth.bottom, 0); + final float innerBottomLeftRadiusX = Math.max(bottomLeftRadius - borderWidth.left, 0); + final float innerBottomLeftRadiusY = Math.max(bottomLeftRadius - borderWidth.bottom, 0); + + mInnerClipPathForBorderRadius.addRoundRect( + mInnerClipTempRectForBorderRadius, + new float[] { + innerTopLeftRadiusX, + innerTopLeftRadiusY, + innerTopRightRadiusX, + innerTopRightRadiusY, + innerBottomRightRadiusX, + innerBottomRightRadiusY, + innerBottomLeftRadiusX, + innerBottomLeftRadiusY, + }, + Path.Direction.CW); + + // There is a small gap between mBackgroundColorRenderPath and its + // border. mGapBetweenPaths is used to slightly enlarge the rectangle + // (mInnerClipTempRectForBorderRadius), ensuring the border can be + // drawn on top without the gap. + mBackgroundColorRenderPath.addRoundRect( + mInnerClipTempRectForBorderRadius.left - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.top - mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.right + mGapBetweenPaths, + mInnerClipTempRectForBorderRadius.bottom + mGapBetweenPaths, + new float[] { + innerTopLeftRadiusX, + innerTopLeftRadiusY, + innerTopRightRadiusX, + innerTopRightRadiusY, + innerBottomRightRadiusX, + innerBottomRightRadiusY, + innerBottomLeftRadiusX, + innerBottomLeftRadiusY, + }, + Path.Direction.CW); + + mOuterClipPathForBorderRadius.addRoundRect( + mOuterClipTempRectForBorderRadius, + new float[] { + topLeftRadius, + topLeftRadius, + topRightRadius, + topRightRadius, + bottomRightRadius, + bottomRightRadius, + bottomLeftRadius, + bottomLeftRadius + }, + Path.Direction.CW); + + float extraRadiusForOutline = 0; + + if (mBorderWidth != null) { + extraRadiusForOutline = mBorderWidth.get(Spacing.ALL) / 2f; + } + + mPathForBorderRadiusOutline.addRoundRect( + mTempRectForBorderRadiusOutline, + new float[] { + topLeftRadius + extraRadiusForOutline, + topLeftRadius + extraRadiusForOutline, + topRightRadius + extraRadiusForOutline, + topRightRadius + extraRadiusForOutline, + bottomRightRadius + extraRadiusForOutline, + bottomRightRadius + extraRadiusForOutline, + bottomLeftRadius + extraRadiusForOutline, + bottomLeftRadius + extraRadiusForOutline + }, + Path.Direction.CW); + + mCenterDrawPath.addRoundRect( + mTempRectForCenterDrawPath, + new float[] { + Math.max( + topLeftRadius - borderWidth.left * 0.5f, + (borderWidth.left > 0.0f) ? (topLeftRadius / borderWidth.left) : 0.0f), + Math.max( + topLeftRadius - borderWidth.top * 0.5f, + (borderWidth.top > 0.0f) ? (topLeftRadius / borderWidth.top) : 0.0f), + Math.max( + topRightRadius - borderWidth.right * 0.5f, + (borderWidth.right > 0.0f) ? (topRightRadius / borderWidth.right) : 0.0f), + Math.max( + topRightRadius - borderWidth.top * 0.5f, + (borderWidth.top > 0.0f) ? (topRightRadius / borderWidth.top) : 0.0f), + Math.max( + bottomRightRadius - borderWidth.right * 0.5f, + (borderWidth.right > 0.0f) ? (bottomRightRadius / borderWidth.right) : 0.0f), + Math.max( + bottomRightRadius - borderWidth.bottom * 0.5f, + (borderWidth.bottom > 0.0f) ? (bottomRightRadius / borderWidth.bottom) : 0.0f), + Math.max( + bottomLeftRadius - borderWidth.left * 0.5f, + (borderWidth.left > 0.0f) ? (bottomLeftRadius / borderWidth.left) : 0.0f), + Math.max( + bottomLeftRadius - borderWidth.bottom * 0.5f, + (borderWidth.bottom > 0.0f) ? (bottomLeftRadius / borderWidth.bottom) : 0.0f) + }, + Path.Direction.CW); + + /** + * Rounded Multi-Colored Border Algorithm: + * + *

Let O (for outer) = (top, left, bottom, right) be the rectangle that represents the size + * and position of a view V. Since the box-sizing of all React Native views is border-box, any + * border of V will render inside O. + * + *

Let BorderWidth = (borderTop, borderLeft, borderBottom, borderRight). + * + *

Let I (for inner) = O - BorderWidth. + * + *

Then, remembering that O and I are rectangles and that I is inside O, O - I gives us the + * border of V. Therefore, we can use canvas.clipPath to draw V's border. + * + *

canvas.clipPath(O, Region.OP.INTERSECT); + * + *

canvas.clipPath(I, Region.OP.DIFFERENCE); + * + *

canvas.drawRect(O, paint); + * + *

This lets us draw non-rounded single-color borders. + * + *

To extend this algorithm to rounded single-color borders, we: + * + *

1. Curve the corners of O by the (border radii of V) using Path#addRoundRect. + * + *

2. Curve the corners of I by (border radii of V - border widths of V) using + * Path#addRoundRect. + * + *

Let O' = curve(O, border radii of V). + * + *

Let I' = curve(I, border radii of V - border widths of V) + * + *

The rationale behind this decision is the (first sentence of the) following section in the + * CSS Backgrounds and Borders Module Level 3: + * https://www.w3.org/TR/css3-background/#the-border-radius. + * + *

After both O and I have been curved, we can execute the following lines once again to + * render curved single-color borders: + * + *

canvas.clipPath(O, Region.OP.INTERSECT); + * + *

canvas.clipPath(I, Region.OP.DIFFERENCE); + * + *

canvas.drawRect(O, paint); + * + *

To extend this algorithm to rendering multi-colored rounded borders, we render each side + * of the border as its own quadrilateral. Suppose that we were handling the case where all the + * border radii are 0. Then, the four quadrilaterals would be: + * + *

Left: (O.left, O.top), (I.left, I.top), (I.left, I.bottom), (O.left, O.bottom) + * + *

Top: (O.left, O.top), (I.left, I.top), (I.right, I.top), (O.right, O.top) + * + *

Right: (O.right, O.top), (I.right, I.top), (I.right, I.bottom), (O.right, O.bottom) + * + *

Bottom: (O.right, O.bottom), (I.right, I.bottom), (I.left, I.bottom), (O.left, O.bottom) + * + *

Now, lets consider what happens when we render a rounded border (radii != 0). For the sake + * of simplicity, let's focus on the top edge of the Left border: + * + *

Let borderTopLeftRadius = 5. Let borderLeftWidth = 1. Let borderTopWidth = 2. + * + *

We know that O is curved by the ellipse E_O (a = 5, b = 5). We know that I is curved by + * the ellipse E_I (a = 5 - 1, b = 5 - 2). + * + *

Since we have clipping, it should be safe to set the top-left point of the Left + * quadrilateral's top edge to (O.left, O.top). + * + *

But, what should the top-right point be? + * + *

The fact that the border is curved shouldn't change the slope (nor the position) of the + * line connecting the top-left and top-right points of the Left quadrilateral's top edge. + * Therefore, The top-right point should lie somewhere on the line L = (1 - a) * (O.left, O.top) + * + a * (I.left, I.top). + * + *

a != 0, because then the top-left and top-right points would be the same and + * borderLeftWidth = 1. a != 1, because then the top-right point would not touch an edge of the + * ellipse E_I. We want the top-right point to touch an edge of the inner ellipse because the + * border curves with E_I on the top-left corner of V. + * + *

Therefore, it must be the case that a > 1. Two natural locations of the top-right point + * exist: 1. The first intersection of L with E_I. 2. The second intersection of L with E_I. + * + *

We choose the top-right point of the top edge of the Left quadrilateral to be an arbitrary + * intersection of L with E_I. + */ + if (mInnerTopLeftCorner == null) { + mInnerTopLeftCorner = new PointF(); + } + + /** Compute mInnerTopLeftCorner */ + mInnerTopLeftCorner.x = mInnerClipTempRectForBorderRadius.left; + mInnerTopLeftCorner.y = mInnerClipTempRectForBorderRadius.top; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.top, + mInnerClipTempRectForBorderRadius.left + 2 * innerTopLeftRadiusX, + mInnerClipTempRectForBorderRadius.top + 2 * innerTopLeftRadiusY, + + // Line Start + mOuterClipTempRectForBorderRadius.left, + mOuterClipTempRectForBorderRadius.top, + + // Line End + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.top, + + // Result + mInnerTopLeftCorner); + + /** Compute mInnerBottomLeftCorner */ + if (mInnerBottomLeftCorner == null) { + mInnerBottomLeftCorner = new PointF(); + } + + mInnerBottomLeftCorner.x = mInnerClipTempRectForBorderRadius.left; + mInnerBottomLeftCorner.y = mInnerClipTempRectForBorderRadius.bottom; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomLeftRadiusY, + mInnerClipTempRectForBorderRadius.left + 2 * innerBottomLeftRadiusX, + mInnerClipTempRectForBorderRadius.bottom, + + // Line Start + mOuterClipTempRectForBorderRadius.left, + mOuterClipTempRectForBorderRadius.bottom, + + // Line End + mInnerClipTempRectForBorderRadius.left, + mInnerClipTempRectForBorderRadius.bottom, + + // Result + mInnerBottomLeftCorner); + + /** Compute mInnerTopRightCorner */ + if (mInnerTopRightCorner == null) { + mInnerTopRightCorner = new PointF(); + } + + mInnerTopRightCorner.x = mInnerClipTempRectForBorderRadius.right; + mInnerTopRightCorner.y = mInnerClipTempRectForBorderRadius.top; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.right - 2 * innerTopRightRadiusX, + mInnerClipTempRectForBorderRadius.top, + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.top + 2 * innerTopRightRadiusY, + + // Line Start + mOuterClipTempRectForBorderRadius.right, + mOuterClipTempRectForBorderRadius.top, + + // Line End + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.top, + + // Result + mInnerTopRightCorner); + + /** Compute mInnerBottomRightCorner */ + if (mInnerBottomRightCorner == null) { + mInnerBottomRightCorner = new PointF(); + } + + mInnerBottomRightCorner.x = mInnerClipTempRectForBorderRadius.right; + mInnerBottomRightCorner.y = mInnerClipTempRectForBorderRadius.bottom; + + getEllipseIntersectionWithLine( + // Ellipse Bounds + mInnerClipTempRectForBorderRadius.right - 2 * innerBottomRightRadiusX, + mInnerClipTempRectForBorderRadius.bottom - 2 * innerBottomRightRadiusY, + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.bottom, + + // Line Start + mOuterClipTempRectForBorderRadius.right, + mOuterClipTempRectForBorderRadius.bottom, + + // Line End + mInnerClipTempRectForBorderRadius.right, + mInnerClipTempRectForBorderRadius.bottom, + + // Result + mInnerBottomRightCorner); + } + + private static void getEllipseIntersectionWithLine( + double ellipseBoundsLeft, + double ellipseBoundsTop, + double ellipseBoundsRight, + double ellipseBoundsBottom, + double lineStartX, + double lineStartY, + double lineEndX, + double lineEndY, + PointF result) { + final double ellipseCenterX = (ellipseBoundsLeft + ellipseBoundsRight) / 2; + final double ellipseCenterY = (ellipseBoundsTop + ellipseBoundsBottom) / 2; + + /** + * Step 1: + * + *

Translate the line so that the ellipse is at the origin. + * + *

Why? It makes the math easier by changing the ellipse equation from ((x - + * ellipseCenterX)/a)^2 + ((y - ellipseCenterY)/b)^2 = 1 to (x/a)^2 + (y/b)^2 = 1. + */ + lineStartX -= ellipseCenterX; + lineStartY -= ellipseCenterY; + lineEndX -= ellipseCenterX; + lineEndY -= ellipseCenterY; + + /** + * Step 2: + * + *

Ellipse equation: (x/a)^2 + (y/b)^2 = 1 Line equation: y = mx + c + */ + final double a = Math.abs(ellipseBoundsRight - ellipseBoundsLeft) / 2; + final double b = Math.abs(ellipseBoundsBottom - ellipseBoundsTop) / 2; + final double m = (lineEndY - lineStartY) / (lineEndX - lineStartX); + final double c = lineStartY - m * lineStartX; // Just a point on the line + + /** + * Step 3: + * + *

Substitute the Line equation into the Ellipse equation. Solve for x. Eventually, you'll + * have to use the quadratic formula. + * + *

Quadratic formula: Ax^2 + Bx + C = 0 + */ + final double A = (b * b + a * a * m * m); + final double B = 2 * a * a * c * m; + final double C = (a * a * (c * c - b * b)); + + /** + * Step 4: + * + *

Apply Quadratic formula. D = determinant / 2A + */ + final double D = Math.sqrt(-C / A + Math.pow(B / (2 * A), 2)); + final double x2 = -B / (2 * A) - D; + final double y2 = m * x2 + c; + + /** + * Step 5: + * + *

Undo the space transformation in Step 5. + */ + final double x = x2 + ellipseCenterX; + final double y = y2 + ellipseCenterY; + + if (!Double.isNaN(x) && !Double.isNaN(y)) { + result.x = (float) x; + result.y = (float) y; + } + } + + public float getBorderWidthOrDefaultTo(final float defaultValue, final int spacingType) { + if (mBorderWidth == null) { + return defaultValue; + } + + final float width = mBorderWidth.getRaw(spacingType); + + if (YogaConstants.isUndefined(width)) { + return defaultValue; + } + + return width; + } + + /** Set type of border */ + private void updatePathEffect() { + // Used for rounded border and rounded background + PathEffect mPathEffectForBorderStyle = + mBorderStyle != null ? BorderStyle.getPathEffect(mBorderStyle, getFullBorderWidth()) : null; + + mPaint.setPathEffect(mPathEffectForBorderStyle); + } + + private void updatePathEffect(int borderWidth) { + PathEffect pathEffectForBorderStyle = null; + if (mBorderStyle != null) { + pathEffectForBorderStyle = BorderStyle.getPathEffect(mBorderStyle, borderWidth); + } + mPaint.setPathEffect(pathEffectForBorderStyle); + } + + /** For rounded borders we use default "borderWidth" property. */ + public float getFullBorderWidth() { + return (mBorderWidth != null && !YogaConstants.isUndefined(mBorderWidth.getRaw(Spacing.ALL))) + ? mBorderWidth.getRaw(Spacing.ALL) + : 0f; + } + + /** + * Quickly determine if all the set border colors are equal. Bitwise AND all the set colors + * together, then OR them all together. If the AND and the OR are the same, then the colors are + * compatible, so return this color. + * + *

Used to avoid expensive path creation and expensive calls to canvas.drawPath + * + * @return A compatible border color, or zero if the border colors are not compatible. + */ + private static long fastBorderCompatibleColorOrZero( + int borderLeft, + int borderTop, + int borderRight, + int borderBottom, + long colorLeft, + long colorTop, + long colorRight, + long colorBottom) { + long andSmear = + (borderLeft > 0 ? colorLeft : ALL_BITS_SET) + & (borderTop > 0 ? colorTop : ALL_BITS_SET) + & (borderRight > 0 ? colorRight : ALL_BITS_SET) + & (borderBottom > 0 ? colorBottom : ALL_BITS_SET); + long orSmear = + (borderLeft > 0 ? colorLeft : ALL_BITS_UNSET) + | (borderTop > 0 ? colorTop : ALL_BITS_UNSET) + | (borderRight > 0 ? colorRight : ALL_BITS_UNSET) + | (borderBottom > 0 ? colorBottom : ALL_BITS_UNSET); + return andSmear == orSmear ? andSmear : 0; + } + + private void drawRectangularBackgroundWithBorders(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); + + long color = useColor(); + if (Color.alpha(color) != 0.0f) { // color is not transparent + mPaint.setColor(color); + canvas.drawRect(getBounds(), mPaint); + } + + final RectF borderWidth = getDirectionAwareBorderInsets(); + + final int borderLeft = Math.round(borderWidth.left); + final int borderTop = Math.round(borderWidth.top); + final int borderRight = Math.round(borderWidth.right); + final int borderBottom = Math.round(borderWidth.bottom); + + // maybe draw borders? + if (borderLeft > 0 || borderRight > 0 || borderTop > 0 || borderBottom > 0) { + Rect bounds = getBounds(); + + long colorLeft = getBorderColor(Spacing.LEFT); + long colorTop = getBorderColor(Spacing.TOP); + long colorRight = getBorderColor(Spacing.RIGHT); + long colorBottom = getBorderColor(Spacing.BOTTOM); + + long colorBlock = getBorderColor(Spacing.BLOCK); + long colorBlockStart = getBorderColor(Spacing.BLOCK_START); + long colorBlockEnd = getBorderColor(Spacing.BLOCK_END); + + if (isBorderColorDefined(Spacing.BLOCK)) { + colorBottom = colorBlock; + colorTop = colorBlock; + } + if (isBorderColorDefined(Spacing.BLOCK_END)) { + colorBottom = colorBlockEnd; + } + if (isBorderColorDefined(Spacing.BLOCK_START)) { + colorTop = colorBlockStart; + } + + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + long colorStart = getBorderColor(Spacing.START); + long colorEnd = getBorderColor(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (!isBorderColorDefined(Spacing.START)) { + colorStart = colorLeft; + } + + if (!isBorderColorDefined(Spacing.END)) { + colorEnd = colorRight; + } + + final long directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final long directionAwareColorRight = isRTL ? colorStart : colorEnd; + + colorLeft = directionAwareColorLeft; + colorRight = directionAwareColorRight; + } else { + final long directionAwareColorLeft = isRTL ? colorEnd : colorStart; + final long directionAwareColorRight = isRTL ? colorStart : colorEnd; + + final boolean isColorStartDefined = isBorderColorDefined(Spacing.START); + final boolean isColorEndDefined = isBorderColorDefined(Spacing.END); + final boolean isDirectionAwareColorLeftDefined = + isRTL ? isColorEndDefined : isColorStartDefined; + final boolean isDirectionAwareColorRightDefined = + isRTL ? isColorStartDefined : isColorEndDefined; + + if (isDirectionAwareColorLeftDefined) { + colorLeft = directionAwareColorLeft; + } + + if (isDirectionAwareColorRightDefined) { + colorRight = directionAwareColorRight; + } + } + + int left = bounds.left; + int top = bounds.top; + + // Check for fast path to border drawing. + long fastBorderColor = + fastBorderCompatibleColorOrZero( + borderLeft, + borderTop, + borderRight, + borderBottom, + colorLeft, + colorTop, + colorRight, + colorBottom); + + if (fastBorderColor != 0) { + if (Color.alpha(fastBorderColor) != 0) { + // Border color is not transparent. + int right = bounds.right; + int bottom = bounds.bottom; + + mPaint.setColor(fastBorderColor); + mPaint.setStyle(Paint.Style.STROKE); + if (borderLeft > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.left); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left + width / 2, top); + mPathForSingleBorder.lineTo(left + width / 2, bottom); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderTop > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.top); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left, top + width / 2); + mPathForSingleBorder.lineTo(right, top + width / 2); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderRight > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.right); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(right - width / 2, top); + mPathForSingleBorder.lineTo(right - width / 2, bottom); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + if (borderBottom > 0) { + mPathForSingleBorder.reset(); + int width = Math.round(borderWidth.bottom); + updatePathEffect(width); + mPaint.setStrokeWidth(width); + mPathForSingleBorder.moveTo(left, bottom - width / 2); + mPathForSingleBorder.lineTo(right, bottom - width / 2); + canvas.drawPath(mPathForSingleBorder, mPaint); + } + } + } else { + // If the path drawn previously is of the same color, + // there would be a slight white space between borders + // with anti-alias set to true. + // Therefore we need to disable anti-alias, and + // after drawing is done, we will re-enable it. + + mPaint.setAntiAlias(false); + + int width = bounds.width(); + int height = bounds.height(); + + if (borderLeft > 0) { + final float x1 = left; + final float y1 = top; + final float x2 = left + borderLeft; + final float y2 = top + borderTop; + final float x3 = left + borderLeft; + final float y3 = top + height - borderBottom; + final float x4 = left; + final float y4 = top + height; + + drawQuadrilateral(canvas, colorLeft, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderTop > 0) { + final float x1 = left; + final float y1 = top; + final float x2 = left + borderLeft; + final float y2 = top + borderTop; + final float x3 = left + width - borderRight; + final float y3 = top + borderTop; + final float x4 = left + width; + final float y4 = top; + + drawQuadrilateral(canvas, colorTop, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderRight > 0) { + final float x1 = left + width; + final float y1 = top; + final float x2 = left + width; + final float y2 = top + height; + final float x3 = left + width - borderRight; + final float y3 = top + height - borderBottom; + final float x4 = left + width - borderRight; + final float y4 = top + borderTop; + + drawQuadrilateral(canvas, colorRight, x1, y1, x2, y2, x3, y3, x4, y4); + } + + if (borderBottom > 0) { + final float x1 = left; + final float y1 = top + height; + final float x2 = left + width; + final float y2 = top + height; + final float x3 = left + width - borderRight; + final float y3 = top + height - borderBottom; + final float x4 = left + borderLeft; + final float y4 = top + height - borderBottom; + + drawQuadrilateral(canvas, colorBottom, x1, y1, x2, y2, x3, y3, x4, y4); + } + + // re-enable anti alias + mPaint.setAntiAlias(true); + } + } + } + + private void drawQuadrilateral( + Canvas canvas, + long fillColor, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3, + float x4, + float y4) { + if (fillColor == Color.TRANSPARENT) { + return; + } + + if (mPathForBorder == null) { + mPathForBorder = new Path(); + } + + mPaint.setColor(fillColor); + mPathForBorder.reset(); + mPathForBorder.moveTo(x1, y1); + mPathForBorder.lineTo(x2, y2); + mPathForBorder.lineTo(x3, y3); + mPathForBorder.lineTo(x4, y4); + mPathForBorder.lineTo(x1, y1); + canvas.drawPath(mPathForBorder, mPaint); + } + + private int getBorderWidth(int position) { + if (mBorderWidth == null) { + return 0; + } + + final float width = mBorderWidth.get(position); + return YogaConstants.isUndefined(width) ? -1 : Math.round(width); + } + + private boolean isBorderColorDefined(int position) { + long color = mBorderColor != null ? mBorderColor.get(position) : 0; + return color != 0; + } + + public long getBorderColor(int position) { + return mBorderColor != null ? mBorderColor.get(position) : DEFAULT_BORDER_COLOR; + } + + public RectF getDirectionAwareBorderInsets() { + final float borderWidth = getBorderWidthOrDefaultTo(0, Spacing.ALL); + final float borderTopWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.TOP); + final float borderBottomWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.BOTTOM); + float borderLeftWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.LEFT); + float borderRightWidth = getBorderWidthOrDefaultTo(borderWidth, Spacing.RIGHT); + + if (mBorderWidth != null) { + final boolean isRTL = getResolvedLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + float borderStartWidth = mBorderWidth.getRaw(Spacing.START); + float borderEndWidth = mBorderWidth.getRaw(Spacing.END); + + if (I18nUtil.getInstance().doLeftAndRightSwapInRTL(mContext)) { + if (YogaConstants.isUndefined(borderStartWidth)) { + borderStartWidth = borderLeftWidth; + } + + if (YogaConstants.isUndefined(borderEndWidth)) { + borderEndWidth = borderRightWidth; + } + + final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; + final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; + + borderLeftWidth = directionAwareBorderLeftWidth; + borderRightWidth = directionAwareBorderRightWidth; + } else { + final float directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth; + final float directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth; + + if (!YogaConstants.isUndefined(directionAwareBorderLeftWidth)) { + borderLeftWidth = directionAwareBorderLeftWidth; + } + + if (!YogaConstants.isUndefined(directionAwareBorderRightWidth)) { + borderRightWidth = directionAwareBorderRightWidth; + } + } + } + + return new RectF(borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth); + } + } \ No newline at end of file diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java index 5c616597d502cd..fc9eaf83ea7131 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewBackgroundManager.java @@ -57,6 +57,14 @@ public void setBackgroundColor(int color) { } } + public void setBackgroundColor(long color) { + if (color == Color.pack(Color.TRANSPARENT) && mReactBackgroundDrawable == null) { + // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background + } else { + getOrCreateReactViewBackground().setColor(color); + } + } + public int getBackgroundColor() { return mColor; } @@ -65,11 +73,11 @@ public void setBorderWidth(int position, float width) { getOrCreateReactViewBackground().setBorderWidth(position, width); } - public void setBorderColor(int position, float color, float alpha) { - getOrCreateReactViewBackground().setBorderColor(position, color, alpha); + public void setBorderColor(int position, long color) { + getOrCreateReactViewBackground().setBorderColor(position, color); } - public int getBorderColor(int position) { + public long getBorderColor(int position) { return getOrCreateReactViewBackground().getBorderColor(position); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java index 793a0f9a584300..3cc3a235da3e61 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java @@ -230,6 +230,14 @@ public void setBackgroundColor(int color) { } } + public void setBackgroundColor(long color) { + if (color == Color.pack(Color.TRANSPARENT) && mReactBackgroundDrawable == null) { + // don't do anything, no need to allocate ReactBackgroundDrawable for transparent background + } else { + getOrCreateReactViewBackground().setColor(color); + } + } + @Override public void setBackground(Drawable drawable) { throw new UnsupportedOperationException( @@ -311,8 +319,8 @@ public void setBorderWidth(int position, float width) { getOrCreateReactViewBackground().setBorderWidth(position, width); } - public void setBorderColor(int position, float rgb, float alpha) { - getOrCreateReactViewBackground().setBorderColor(position, rgb, alpha); + public void setBorderColor(int position, long color) { + getOrCreateReactViewBackground().setBorderColor(position, color); } public void setBorderRadius(float borderRadius) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index dde735192aafa6..423f0e0ecac312 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -252,11 +252,9 @@ public void setBorderWidth(ReactViewGroup view, int index, float width) { ViewProps.BORDER_BLOCK_START_COLOR }, customType = "Color") - public void setBorderColor(ReactViewGroup view, int index, Integer color) { - float rgbComponent = - color == null ? YogaConstants.UNDEFINED : (float) ((int) color & 0x00FFFFFF); - float alphaComponent = color == null ? YogaConstants.UNDEFINED : (float) ((int) color >>> 24); - view.setBorderColor(SPACING_TYPES[index], rgbComponent, alphaComponent); + public void setBorderColor(ReactViewGroup view, int index, Long color) { + long borderColor = color == null ? 0 : color; + view.setBorderColor(SPACING_TYPES[index], borderColor); } @ReactProp(name = ViewProps.COLLAPSABLE) diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 2edfdf69c44646..d2874acbd22e6e 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -1029,11 +1029,11 @@ inline MapBuffer toMapBuffer(const FontVariant& fontVariant) { inline MapBuffer toMapBuffer(const TextAttributes& textAttributes) { auto builder = MapBufferBuilder(); if (textAttributes.foregroundColor) { - builder.putInt( + builder.putLong( TA_KEY_FOREGROUND_COLOR, toAndroidRepr(textAttributes.foregroundColor)); } if (textAttributes.backgroundColor) { - builder.putInt( + builder.putLong( TA_KEY_BACKGROUND_COLOR, toAndroidRepr(textAttributes.backgroundColor)); } if (!std::isnan(textAttributes.opacity)) { @@ -1089,7 +1089,7 @@ inline MapBuffer toMapBuffer(const TextAttributes& textAttributes) { // Decoration if (textAttributes.textDecorationColor) { - builder.putInt( + builder.putLong( TA_KEY_TEXT_DECORATION_COLOR, toAndroidRepr(textAttributes.textDecorationColor)); } @@ -1110,7 +1110,7 @@ inline MapBuffer toMapBuffer(const TextAttributes& textAttributes) { TA_KEY_TEXT_SHADOW_RADIUS, textAttributes.textShadowRadius); } if (textAttributes.textShadowColor) { - builder.putInt( + builder.putLong( TA_KEY_TEXT_SHADOW_COLOR, toAndroidRepr(textAttributes.textShadowColor)); } diff --git a/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h b/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h index 8e1ff0a04739bc..15eb9f7995e007 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h +++ b/packages/react-native/ReactCommon/react/renderer/core/graphicsConversions.h @@ -34,7 +34,7 @@ inline void fromRawValue( } #ifdef ANDROID -inline int toAndroidRepr(const SharedColor& color) { +inline int64_t toAndroidRepr(const SharedColor& color) { return *color; } inline folly::dynamic toDynamic(const SharedColor& color) { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.cpp new file mode 100644 index 00000000000000..d0f9c3a5219fa3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.cpp @@ -0,0 +1,22 @@ +/* + * 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. + */ + +#include "ColorComponents.h" + +namespace facebook::react { + +static ColorSpace defaultColorSpace = ColorSpace::sRGB; + +ColorSpace getDefaultColorSpace() { + return defaultColorSpace; +} + +void setDefaultColorSpace(ColorSpace newColorSpace) { + defaultColorSpace = newColorSpace; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.h b/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.h index 0a3887a3caeb7e..382045ecaa868b 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ColorComponents.h @@ -9,11 +9,18 @@ namespace facebook::react { +enum class ColorSpace { sRGB, DisplayP3 }; + +// Declare the functions without providing definitions +ColorSpace getDefaultColorSpace(); +void setDefaultColorSpace(ColorSpace newColorSpace); + struct ColorComponents { float red{0}; float green{0}; float blue{0}; float alpha{0}; + ColorSpace colorSpace{getDefaultColorSpace()}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/fromRawValueShared.h b/packages/react-native/ReactCommon/react/renderer/graphics/fromRawValueShared.h index fbc07288fa318b..800e0ab2e0d613 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/fromRawValueShared.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/fromRawValueShared.h @@ -44,6 +44,24 @@ inline void fromRawValueShared( result = colorFromComponents(colorComponents); } else { + if (value.hasType>()) { + auto items = (std::unordered_map)value; + if (items.find("space") != items.end()) { + colorComponents.red = (float)items.at("r"); + colorComponents.green = (float)items.at("g"); + colorComponents.blue = (float)items.at("b"); + colorComponents.alpha = (float)items.at("a"); + colorComponents.colorSpace = getDefaultColorSpace(); + std::string space = (std::string)items.at("space"); + if (space == "display-p3") { + colorComponents.colorSpace = ColorSpace::DisplayP3; + } else if (space == "srgb") { + colorComponents.colorSpace = ColorSpace::sRGB; + } + result = colorFromComponents(colorComponents); + return; + } + } result = parsePlatformColor(contextContainer, surfaceId, value); } } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/android/react/renderer/graphics/HostPlatformColor.h b/packages/react-native/ReactCommon/react/renderer/graphics/platform/android/react/renderer/graphics/HostPlatformColor.h index 0dd7abe08f05d0..f64a632f628347 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/android/react/renderer/graphics/HostPlatformColor.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/android/react/renderer/graphics/HostPlatformColor.h @@ -12,7 +12,7 @@ namespace facebook::react { -using Color = int32_t; +using Color = int64_t; namespace HostPlatformColor { static const facebook::react::Color UndefinedColor = @@ -20,20 +20,51 @@ static const facebook::react::Color UndefinedColor = } inline Color hostPlatformColorFromComponents(ColorComponents components) { - float ratio = 255; - return ((int)round(components.alpha * ratio) & 0xff) << 24 | - ((int)round(components.red * ratio) & 0xff) << 16 | - ((int)round(components.green * ratio) & 0xff) << 8 | - ((int)round(components.blue * ratio) & 0xff); + if (components.colorSpace == ColorSpace::DisplayP3) { + int ratio = 15360; + int red = static_cast(round(components.red * ratio)) & 0xffff; + int green = static_cast(round(components.green * ratio)) & 0xffff; + int blue = static_cast(round(components.blue * ratio)) & 0xffff; + int alpha = static_cast(round(components.alpha * 0x3ff)) & 0x3ff; + int colorSpace = 7; + int64_t androidColor = (static_cast(red) << 48) | + (static_cast(green) << 32) | + (static_cast(blue) << 16) | + (static_cast(alpha) << 6) | + static_cast(colorSpace); + return androidColor; + } else { + int ratio = 255; + int alpha = static_cast(round(components.alpha * ratio)) & 0xff; + int red = static_cast(round(components.red * ratio)) & 0xff; + int green = static_cast(round(components.green * ratio)) & 0xff; + int blue = static_cast(round(components.blue * ratio)) & 0xff; + int64_t androidColor = (static_cast(alpha) << 56) | + (static_cast(red) << 48) | + (static_cast(green) << 40) | + (static_cast(blue) << 32); + return androidColor; + } } inline ColorComponents colorComponentsFromHostPlatformColor(Color color) { - float ratio = 255; - return ColorComponents{ - (float)((color >> 16) & 0xff) / ratio, - (float)((color >> 8) & 0xff) / ratio, - (float)((color >> 0) & 0xff) / ratio, - (float)((color >> 24) & 0xff) / ratio}; + if ((color & 0x3f) == 7) { + int ratio = 15360; + return ColorComponents{ + (float)((color >> 48) & 0xffff) / ratio, + (float)((color >> 32) & 0xffff) / ratio, + (float)((color >> 16) & 0xffff) / ratio, + (float)((color >> 6) & 0x3ff) / ratio, + ColorSpace::DisplayP3}; + } else { + int ratio = 255; + return ColorComponents{ + (float)((color >> 48) & 0xff) / ratio, + (float)((color >> 40) & 0xff) / ratio, + (float)((color >> 32) & 0xff) / ratio, + (float)((color >> 56) & 0xff) / ratio, + ColorSpace::sRGB}; + } } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp index 8a46d17fa3b0da..5d9c0aa56ef912 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp @@ -61,6 +61,14 @@ int32_t MapBuffer::getInt(Key key) const { bytes_.data() + valueOffset(bucketIndex)); } +int64_t MapBuffer::getLong(Key key) const { + auto bucketIndex = getKeyBucket(key); + react_native_assert(bucketIndex != -1 && "Key not found in MapBuffer"); + + return *reinterpret_cast( + bytes_.data() + valueOffset(bucketIndex)); +} + bool MapBuffer::getBool(Key key) const { return getInt(key) != 0; } diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index 1efe45cf063114..968747ee64fb71 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -104,6 +104,7 @@ class MapBuffer { Double = 2, String = 3, Map = 4, + Long = 5, }; explicit MapBuffer(std::vector data); @@ -118,6 +119,8 @@ class MapBuffer { int32_t getInt(MapBuffer::Key key) const; + int64_t getLong(MapBuffer::Key key) const; + bool getBool(MapBuffer::Key key) const; double getDouble(MapBuffer::Key key) const; diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp index 7bf76d91e6907e..f7eb5a0e4a8405 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp @@ -15,6 +15,7 @@ namespace facebook::react { constexpr uint32_t INT_SIZE = sizeof(uint32_t); constexpr uint32_t DOUBLE_SIZE = sizeof(double); constexpr uint32_t MAX_BUCKET_VALUE_SIZE = sizeof(uint64_t); +constexpr uint64_t LONG_SIZE = sizeof(uint64_t); MapBuffer MapBufferBuilder::EMPTY() { return MapBufferBuilder(0).build(); @@ -76,6 +77,14 @@ void MapBufferBuilder::putInt(MapBuffer::Key key, int32_t value) { INT_SIZE); } +void MapBufferBuilder::putLong(MapBuffer::Key key, int64_t value) { + storeKeyValue( + key, + MapBuffer::DataType::Long, + reinterpret_cast(&value), + LONG_SIZE); +} + void MapBufferBuilder::putString(MapBuffer::Key key, const std::string& value) { auto strSize = value.size(); const char* strData = value.data(); diff --git a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h index 60ca37d20aba74..2260e690a2d785 100644 --- a/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h +++ b/packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h @@ -27,7 +27,7 @@ class MapBufferBuilder { void putInt(MapBuffer::Key key, int32_t value); - // TODO: Support 64 bit integers + void putLong(MapBuffer::Key key, int64_t value); void putBool(MapBuffer::Key key, bool value);