mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
PlatformColor implementations for iOS and Android (#27908)
Summary: This Pull Request implements the PlatformColor proposal discussed at https://github.com/react-native-community/discussions-and-proposals/issues/126. The changes include implementations for iOS and Android as well as a PlatformColorExample page in RNTester. Every native platform has the concept of system defined colors. Instead of specifying a concrete color value the app developer can choose a system color that varies in appearance depending on a system theme settings such Light or Dark mode, accessibility settings such as a High Contrast mode, and even its context within the app such as the traits of a containing view or window. The proposal is to add true platform color support to react-native by extending the Flow type `ColorValue` with platform specific color type information for each platform and to provide a convenience function, `PlatformColor()`, for instantiating platform specific ColorValue objects. `PlatformColor(name [, name ...])` where `name` is a system color name on a given platform. If `name` does not resolve to a color for any reason, the next `name` in the argument list will be resolved and so on. If none of the names resolve, a RedBox error occurs. This allows a latest platform color to be used, but if running on an older platform it will fallback to a previous version. The function returns a `ColorValue`. On iOS the values of `name` is one of the iOS [UI Element](https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors) or [Standard Color](https://developer.apple.com/documentation/uikit/uicolor/standard_colors) names such as `labelColor` or `systemFillColor`. On Android the `name` values are the same [app resource](https://developer.android.com/guide/topics/resources/providing-resources) path strings that can be expressed in XML: XML Resource: `@ [<package_name>:]<resource_type>/<resource_name>` Style reference from current theme: `?[<package_name>:][<resource_type>/]<resource_name>` For example: - `?android:colorError` - `?android:attr/colorError` - `?attr/colorPrimary` - `?colorPrimaryDark` - `android:color/holo_purple` - `color/catalyst_redbox_background` On iOS another type of system dynamic color can be created using the `IOSDynamicColor({dark: <color>, light:<color>})` method. The arguments are a tuple containing custom colors for light and dark themes. Such dynamic colors are useful for branding colors or other app specific colors that still respond automatically to system setting changes. Example: `<View style={{ backgroundColor: IOSDynamicColor({light: 'black', dark: 'white'}) }}/>` Other platforms could create platform specific functions similar to `IOSDynamicColor` per the needs of those platforms. For example, macOS has a similar dynamic color type that could be implemented via a `MacDynamicColor`. On Windows custom brushes that tint or otherwise modify a system brush could be created using a platform specific method. ## Changelog [General] [Added] - Added PlatformColor implementations for iOS and Android Pull Request resolved: https://github.com/facebook/react-native/pull/27908 Test Plan: The changes have been tested using the RNTester test app for iOS and Android. On iOS a set of XCTestCase's were added to the Unit Tests. <img width="924" alt="PlatformColor-ios-android" src="https://user-images.githubusercontent.com/30053638/73472497-ff183a80-433f-11ea-90d8-2b04338bbe79.png"> In addition `PlatformColor` support has been added to other out-of-tree platforms such as macOS and Windows has been implemented using these changes: react-native for macOS branch: https://github.com/microsoft/react-native/compare/master...tom-un:tomun/platformcolors react-native for Windows branch: https://github.com/microsoft/react-native-windows/compare/master...tom-un:tomun/platformcolors iOS |Light|Dark| |{F229354502}|{F229354515}| Android |Light|Dark| |{F230114392}|{F230114490}| {F230122700} Reviewed By: hramos Differential Revision: D19837753 Pulled By: TheSavior fbshipit-source-id: 82ca70d40802f3b24591bfd4b94b61f3c38ba829
This commit is contained in:
committed by
Facebook Github Bot
parent
5166856d04
commit
f4de45800f
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.bridge;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.TypedValue;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
public class ColorPropConverter {
|
||||
private static final String JSON_KEY = "resource_paths";
|
||||
private static final String PREFIX_RESOURCE = "@";
|
||||
private static final String PREFIX_ATTR = "?";
|
||||
private static final String PACKAGE_DELIMITER = ":";
|
||||
private static final String PATH_DELIMITER = "/";
|
||||
private static final String ATTR = "attr";
|
||||
private static final String ATTR_SEGMENT = "attr/";
|
||||
|
||||
public static Integer getColor(Object value, Context context) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Double) {
|
||||
return ((Double) value).intValue();
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
throw new RuntimeException("Context may not be null.");
|
||||
}
|
||||
|
||||
if (value instanceof ReadableMap) {
|
||||
ReadableMap map = (ReadableMap) value;
|
||||
ReadableArray resourcePaths = map.getArray(JSON_KEY);
|
||||
|
||||
if (resourcePaths == null) {
|
||||
throw new JSApplicationCausedNativeException(
|
||||
"ColorValue: The `" + JSON_KEY + "` must be an array of color resource path strings.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < resourcePaths.size(); i++) {
|
||||
String resourcePath = resourcePaths.getString(i);
|
||||
|
||||
if (resourcePath == null || resourcePath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isResource = resourcePath.startsWith(PREFIX_RESOURCE);
|
||||
boolean isThemeAttribute = resourcePath.startsWith(PREFIX_ATTR);
|
||||
|
||||
resourcePath = resourcePath.substring(1);
|
||||
|
||||
try {
|
||||
if (isResource) {
|
||||
return resolveResource(context, resourcePath);
|
||||
} else if (isThemeAttribute) {
|
||||
return resolveThemeAttribute(context, resourcePath);
|
||||
}
|
||||
} catch (Resources.NotFoundException exception) {
|
||||
// The resource could not be found so do nothing to allow the for loop to continue and
|
||||
// try the next fallback resource in the array. If none of the fallbacks are
|
||||
// found then the exception immediately after the for loop will be thrown.
|
||||
}
|
||||
}
|
||||
|
||||
throw new JSApplicationCausedNativeException(
|
||||
"ColorValue: None of the paths in the `"
|
||||
+ JSON_KEY
|
||||
+ "` array resolved to a color resource.");
|
||||
}
|
||||
|
||||
throw new JSApplicationCausedNativeException(
|
||||
"ColorValue: the value must be a number or Object.");
|
||||
}
|
||||
|
||||
private static int resolveResource(Context context, String resourcePath) {
|
||||
String[] pathTokens = resourcePath.split(PACKAGE_DELIMITER);
|
||||
|
||||
String packageName = context.getPackageName();
|
||||
String resource = resourcePath;
|
||||
|
||||
if (pathTokens.length > 1) {
|
||||
packageName = pathTokens[0];
|
||||
resource = pathTokens[1];
|
||||
}
|
||||
|
||||
String[] resourceTokens = resource.split(PATH_DELIMITER);
|
||||
String resourceType = resourceTokens[0];
|
||||
String resourceName = resourceTokens[1];
|
||||
|
||||
int resourceId = context.getResources().getIdentifier(resourceName, resourceType, packageName);
|
||||
|
||||
return ResourcesCompat.getColor(context.getResources(), resourceId, context.getTheme());
|
||||
}
|
||||
|
||||
private static int resolveThemeAttribute(Context context, String resourcePath) {
|
||||
String path = resourcePath.replaceAll(ATTR_SEGMENT, "");
|
||||
String[] pathTokens = path.split(PACKAGE_DELIMITER);
|
||||
|
||||
String packageName = context.getPackageName();
|
||||
String resourceName = path;
|
||||
|
||||
if (pathTokens.length > 1) {
|
||||
packageName = pathTokens[0];
|
||||
resourceName = pathTokens[1];
|
||||
}
|
||||
|
||||
int resourceId = context.getResources().getIdentifier(resourceName, ATTR, packageName);
|
||||
|
||||
TypedValue outValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
|
||||
if (theme.resolveAttribute(resourceId, outValue, true)) {
|
||||
return outValue.data;
|
||||
}
|
||||
|
||||
throw new Resources.NotFoundException();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -9,6 +9,7 @@ package com.facebook.react.uimanager;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.react.bridge.ColorPropConverter;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.yoga.YogaConstants;
|
||||
@@ -47,7 +48,8 @@ public abstract class BaseViewManagerDelegate<T extends View, U extends BaseView
|
||||
mViewManager.setViewState(view, (ReadableMap) value);
|
||||
break;
|
||||
case ViewProps.BACKGROUND_COLOR:
|
||||
mViewManager.setBackgroundColor(view, value == null ? 0 : ((Double) value).intValue());
|
||||
mViewManager.setBackgroundColor(
|
||||
view, value == null ? 0 : ColorPropConverter.getColor(value, view.getContext()));
|
||||
break;
|
||||
case ViewProps.BORDER_RADIUS:
|
||||
mViewManager.setBorderRadius(
|
||||
|
||||
+46
-15
@@ -7,9 +7,11 @@
|
||||
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.react.bridge.ColorPropConverter;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.DynamicFromObject;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
@@ -81,13 +83,13 @@ import java.util.Map;
|
||||
try {
|
||||
if (mIndex == null) {
|
||||
VIEW_MGR_ARGS[0] = viewToUpdate;
|
||||
VIEW_MGR_ARGS[1] = getValueOrDefault(value);
|
||||
VIEW_MGR_ARGS[1] = getValueOrDefault(value, viewToUpdate.getContext());
|
||||
mSetter.invoke(viewManager, VIEW_MGR_ARGS);
|
||||
Arrays.fill(VIEW_MGR_ARGS, null);
|
||||
} else {
|
||||
VIEW_MGR_GROUP_ARGS[0] = viewToUpdate;
|
||||
VIEW_MGR_GROUP_ARGS[1] = mIndex;
|
||||
VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value);
|
||||
VIEW_MGR_GROUP_ARGS[2] = getValueOrDefault(value, viewToUpdate.getContext());
|
||||
mSetter.invoke(viewManager, VIEW_MGR_GROUP_ARGS);
|
||||
Arrays.fill(VIEW_MGR_GROUP_ARGS, null);
|
||||
}
|
||||
@@ -105,12 +107,12 @@ import java.util.Map;
|
||||
public void updateShadowNodeProp(ReactShadowNode nodeToUpdate, Object value) {
|
||||
try {
|
||||
if (mIndex == null) {
|
||||
SHADOW_ARGS[0] = getValueOrDefault(value);
|
||||
SHADOW_ARGS[0] = getValueOrDefault(value, nodeToUpdate.getThemedContext());
|
||||
mSetter.invoke(nodeToUpdate, SHADOW_ARGS);
|
||||
Arrays.fill(SHADOW_ARGS, null);
|
||||
} else {
|
||||
SHADOW_GROUP_ARGS[0] = mIndex;
|
||||
SHADOW_GROUP_ARGS[1] = getValueOrDefault(value);
|
||||
SHADOW_GROUP_ARGS[1] = getValueOrDefault(value, nodeToUpdate.getThemedContext());
|
||||
mSetter.invoke(nodeToUpdate, SHADOW_GROUP_ARGS);
|
||||
Arrays.fill(SHADOW_GROUP_ARGS, null);
|
||||
}
|
||||
@@ -125,7 +127,7 @@ import java.util.Map;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract @Nullable Object getValueOrDefault(Object value);
|
||||
protected abstract @Nullable Object getValueOrDefault(Object value, Context context);
|
||||
}
|
||||
|
||||
private static class DynamicPropSetter extends PropSetter {
|
||||
@@ -139,7 +141,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value) {
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
if (value instanceof Dynamic) {
|
||||
return value;
|
||||
} else {
|
||||
@@ -163,7 +165,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value) {
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
// All numbers from JS are Doubles which can't be simply cast to Integer
|
||||
return value == null ? mDefaultValue : (Integer) ((Double) value).intValue();
|
||||
}
|
||||
@@ -184,11 +186,34 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value) {
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
return value == null ? mDefaultValue : (Double) value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ColorPropSetter extends PropSetter {
|
||||
|
||||
private final int mDefaultValue;
|
||||
|
||||
public ColorPropSetter(ReactProp prop, Method setter) {
|
||||
this(prop, setter, 0);
|
||||
}
|
||||
|
||||
public ColorPropSetter(ReactProp prop, Method setter, int defaultValue) {
|
||||
super(prop, "mixed", setter);
|
||||
mDefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
if (value == null) {
|
||||
return mDefaultValue;
|
||||
}
|
||||
|
||||
return ColorPropConverter.getColor(value, context);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BooleanPropSetter extends PropSetter {
|
||||
|
||||
private final boolean mDefaultValue;
|
||||
@@ -199,7 +224,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value) {
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
boolean val = value == null ? mDefaultValue : (boolean) value;
|
||||
return val ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
@@ -220,7 +245,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getValueOrDefault(Object value) {
|
||||
protected Object getValueOrDefault(Object value, Context context) {
|
||||
// All numbers from JS are Doubles which can't be simply cast to Float
|
||||
return value == null ? mDefaultValue : (Float) ((Double) value).floatValue();
|
||||
}
|
||||
@@ -233,7 +258,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getValueOrDefault(Object value) {
|
||||
protected @Nullable Object getValueOrDefault(Object value, Context context) {
|
||||
return (ReadableArray) value;
|
||||
}
|
||||
}
|
||||
@@ -245,7 +270,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getValueOrDefault(Object value) {
|
||||
protected @Nullable Object getValueOrDefault(Object value, Context context) {
|
||||
return (ReadableMap) value;
|
||||
}
|
||||
}
|
||||
@@ -257,7 +282,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getValueOrDefault(Object value) {
|
||||
protected @Nullable Object getValueOrDefault(Object value, Context context) {
|
||||
return (String) value;
|
||||
}
|
||||
}
|
||||
@@ -269,7 +294,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getValueOrDefault(Object value) {
|
||||
protected @Nullable Object getValueOrDefault(Object value, Context context) {
|
||||
if (value != null) {
|
||||
return (boolean) value ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
@@ -288,7 +313,7 @@ import java.util.Map;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Object getValueOrDefault(Object value) {
|
||||
protected @Nullable Object getValueOrDefault(Object value, Context context) {
|
||||
if (value != null) {
|
||||
if (value instanceof Double) {
|
||||
return ((Double) value).intValue();
|
||||
@@ -379,6 +404,9 @@ import java.util.Map;
|
||||
} else if (propTypeClass == boolean.class) {
|
||||
return new BooleanPropSetter(annotation, method, annotation.defaultBoolean());
|
||||
} else if (propTypeClass == int.class) {
|
||||
if ("Color".equals(annotation.customType())) {
|
||||
return new ColorPropSetter(annotation, method, annotation.defaultInt());
|
||||
}
|
||||
return new IntPropSetter(annotation, method, annotation.defaultInt());
|
||||
} else if (propTypeClass == float.class) {
|
||||
return new FloatPropSetter(annotation, method, annotation.defaultFloat());
|
||||
@@ -389,6 +417,9 @@ import java.util.Map;
|
||||
} else if (propTypeClass == Boolean.class) {
|
||||
return new BoxedBooleanPropSetter(annotation, method);
|
||||
} else if (propTypeClass == Integer.class) {
|
||||
if ("Color".equals(annotation.customType())) {
|
||||
return new ColorPropSetter(annotation, method);
|
||||
}
|
||||
return new BoxedIntPropSetter(annotation, method);
|
||||
} else if (propTypeClass == ReadableArray.class) {
|
||||
return new ArrayPropSetter(annotation, method);
|
||||
|
||||
+6
-1
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener;
|
||||
import com.facebook.react.bridge.ColorPropConverter;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
@@ -68,7 +69,11 @@ public class SwipeRefreshLayoutManager extends ViewGroupManager<ReactSwipeRefres
|
||||
if (colors != null) {
|
||||
int[] colorValues = new int[colors.size()];
|
||||
for (int i = 0; i < colors.size(); i++) {
|
||||
colorValues[i] = colors.getInt(i);
|
||||
if (colors.getType(i) == ReadableType.Map) {
|
||||
colorValues[i] = ColorPropConverter.getColor(colors.getMap(i), view.getContext());
|
||||
} else {
|
||||
colorValues[i] = colors.getInt(i);
|
||||
}
|
||||
}
|
||||
view.setColorSchemeColors(colorValues);
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -469,7 +469,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.COLOR)
|
||||
@ReactProp(name = ViewProps.COLOR, customType = "Color")
|
||||
public void setColor(@Nullable Integer color) {
|
||||
mIsColorSet = (color != null);
|
||||
if (mIsColorSet) {
|
||||
|
||||
Reference in New Issue
Block a user