Skip to content

Commit

Permalink
PlatformColor Support (wix#7216)
Browse files Browse the repository at this point in the history
Following the discussion here: wix#6618.

## Android
- Colours are parsed natively.
- Creating custom colour when a PlatformColor is passed to native.
- Both light/dark values have the same res attribute resolved when `get` is called.

## IOS
- Added a custom colour.
- Changed storyboard splash screen.
- Using `labelColor` as a test.


CC: @yogevbd 
closes: wix#6618

Co-authored-by: Yogev Ben David <yogev132@gmail.com>
  • Loading branch information
swabbass and yogevbd authored Aug 29, 2021
1 parent 0093684 commit 1db86ce
Show file tree
Hide file tree
Showing 17 changed files with 224 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.reactnativenavigation.options.params

import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReadableMap
import com.reactnativenavigation.NavigationApplication

private fun parsePlatformColor(paths: ReadableMap) =
ColorPropConverter.getColor(paths, NavigationApplication.instance)

class ReactPlatformColor(private val paths: ReadableMap) :
Colour(parsePlatformColor(paths)) {
override fun get(): Int {
return parsePlatformColor(paths)
}

override fun get(defaultValue: Int?): Int? {
return try {
parsePlatformColor(paths)
}catch (e:Exception){
defaultValue
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class NullThemeColour() : ThemeColour(NullColor(), NullColor()) {

open class ThemeColour(private var lightColor: Colour, private var darkColor: Colour) {

constructor(color:Colour):this(color,color)
constructor(color: Colour) : this(color, color)

private fun selectedColor() = if (isDarkMode()) darkColor else lightColor

Expand All @@ -30,18 +30,23 @@ open class ThemeColour(private var lightColor: Colour, private var darkColor: Co
fun hasTransparency() = selectedColor().hasTransparency()
fun canApplyValue() = selectedColor().canApplyValue()

companion object{
companion object {
@JvmStatic
fun of(color:Int) = ThemeColour(Colour(color), Colour(color))
fun of(color: Int) = ThemeColour(Colour(color), Colour(color))

@JvmStatic
fun of(light:Int,dark:Int) = ThemeColour(Colour(light),Colour(dark))
fun of(light: Int, dark: Int) = ThemeColour(Colour(light), Colour(dark))

@JvmStatic
fun parse(context: Context, json: JSONObject?): ThemeColour {
return json?.let {
ThemeColour(ColorParser.parse(context, json, LIGHT_COLOR_KEY), ColorParser.parse(context, json, DARK_COLOR_KEY))
ThemeColour(
ColorParser.parse(context, json, LIGHT_COLOR_KEY),
ColorParser.parse(context, json, DARK_COLOR_KEY)
)
} ?: NullThemeColour()
}

@JvmStatic
fun transparent() = of(Color.TRANSPARENT)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.reactnativenavigation.options.parsers;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
Expand Down Expand Up @@ -98,7 +99,7 @@ private static Object parseNumber(ReadableArray arr, int index) {
}

public static WritableMap convert(JSONObject jsonObject) {
WritableMap map = new WritableNativeMap();
WritableMap map = Arguments.createMap();

Iterator<String> iterator = jsonObject.keys();
while (iterator.hasNext()) {
Expand All @@ -125,7 +126,7 @@ public static WritableMap convert(JSONObject jsonObject) {
}

public static WritableArray convert(JSONArray jsonArray) {
WritableArray array = new WritableNativeArray();
WritableArray array = Arguments.createArray();

for (int i = 0; i < jsonArray.length(); i++) {
Object value = jsonArray.opt(i);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.reactnativenavigation.options.parsers

import android.content.Context
import com.facebook.react.bridge.ColorPropConverter
import com.reactnativenavigation.options.params.Colour
import com.reactnativenavigation.options.params.DontApplyColour
import com.reactnativenavigation.options.params.NullColor
import com.reactnativenavigation.options.params.ReactPlatformColor
import org.json.JSONObject

object ColorParser {
private const val KEY_RESOURCE_PATHS = "resource_paths"
private const val VAL_NO_COLOR = "NoColor"

@JvmStatic
fun parse(context: Context?, json: JSONObject, colorName: String?): Colour {
if (json.has(KEY_RESOURCE_PATHS)) {
return ReactPlatformColor(JSONParser.convert(json))
}
return when (val color = json.opt(colorName)) {
null, VAL_NO_COLOR -> {
DontApplyColour()
}
is Int -> {
Colour(json.optInt(colorName))
}
is JSONObject -> {
ColorPropConverter.getColor(color, context)?.let {
Colour(it)
} ?: NullColor()
}
else -> {
NullColor()
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
Expand All @@ -20,6 +21,7 @@
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
Expand Down Expand Up @@ -58,6 +60,8 @@ public void beforeEach() {
mockConfiguration.uiMode = Configuration.UI_MODE_NIGHT_NO;
when(res.getConfiguration()).thenReturn(mockConfiguration);
when(NavigationApplication.instance.getResources()).thenReturn(res);
when(res.getColor(ArgumentMatchers.anyInt())).thenReturn(0x00000);
when(res.getColor(ArgumentMatchers.anyInt(),any())).thenReturn(0x00000);
}

public void mockStatusBarUtils(int statusBarHeight,int statusBarHeightDp, Functions.Func block) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
package com.reactnativenavigation.options.parsers;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.JavaOnlyMap;
import com.reactnativenavigation.BaseTest;
import com.reactnativenavigation.options.params.DontApplyColour;
import com.reactnativenavigation.options.params.ReactPlatformColor;
import com.reactnativenavigation.utils.StatusBarUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;

import android.app.Activity;

public class ColorParseTest extends BaseTest {

Activity activity;
@Override
public void beforeEach() {
super.beforeEach();
activity = newActivity();
}

@Test
public void nullIsParsedAsNoColor() throws JSONException {
JSONObject json = new JSONObject();
json.put("color", "NoColor");
assertThat(ColorParser.parse(null, json, "color")).isInstanceOf(DontApplyColour.class);
}

@Test
public void shouldParsePlatformColors() throws JSONException {
JSONObject json = new JSONObject();
JSONObject color = new JSONObject();
final JSONArray jsonArray = new JSONArray();
jsonArray.put("@color/colorPrimary");
color.put("resource_paths",
jsonArray);
try (MockedStatic<Arguments> theMock = Mockito.mockStatic(Arguments.class)) {
theMock.when(Arguments::createMap).thenReturn(new JavaOnlyMap());
theMock.when(Arguments::createArray).thenReturn(new JavaOnlyArray());
assertThat(ColorParser.parse(activity, color, "color")).isInstanceOf(ReactPlatformColor.class);
}
}
}
17 changes: 17 additions & 0 deletions lib/src/commands/OptionsProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,23 @@ describe('navigation options', () => {
Platform.OS = 'android';
});

it('PlatformColor should be passed to native as is', () => {
const options: Options = {
topBar: {
background: {
color: {
// @ts-ignore
resource_paths: ['@color/textColor'],
},
},
},
};
uut.processOptions(options, CommandName.SetRoot);
expect(options).toEqual({
topBar: { background: { color: { resource_paths: ['@color/textColor'] } } },
});
});

it('processes color keys', () => {
const options: Options = {
statusBar: { backgroundColor: 'red' },
Expand Down
11 changes: 8 additions & 3 deletions lib/src/commands/OptionsProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,15 @@ export class OptionsProcessor {
if (value === null) {
options[key] = newColorObj;
} else if (value instanceof Object) {
for (let keyColor in value) {
newColorObj[keyColor] = this.colorService.toNativeColor(value[keyColor]);
if ('semantic' in value || 'resource_paths' in value) {
options[key] = value;
return;
} else {
for (let keyColor in value) {
newColorObj[keyColor] = this.colorService.toNativeColor(value[keyColor]);
}
options[key] = newColorObj;
}
options[key] = newColorObj;
} else {
let parsedColor = this.colorService.toNativeColor(value);
newColorObj.light = parsedColor;
Expand Down
4 changes: 2 additions & 2 deletions lib/src/interfaces/Options.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// tslint:disable jsdoc-format
import { ImageRequireSource, ImageSourcePropType, Insets } from 'react-native';
import { ImageRequireSource, ImageSourcePropType, Insets, OpaqueColorValue } from 'react-native';

// TODO: Import ColorValue instead when upgrading @types/react-native to 0.63+
// Only assign PlatformColor or DynamicColorIOS as a Color symbol!
export declare type Color = string | symbol | ThemeColor | null;
export declare type Color = string | symbol | ThemeColor | OpaqueColorValue | null;
type FontFamily = string;
type FontStyle = 'normal' | 'italic';
type FontWeightIOS =
Expand Down
5 changes: 5 additions & 0 deletions playground/android/app/src/main/res/values-night/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="textColor" type="color">#BA292E</item>
<item name="backgroundColor" type="color">#282528</item>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="textColor" type="color">#000000</item>
<item name="backgroundColor" type="color">#e8e8e8</item>
</resources>
5 changes: 5 additions & 0 deletions playground/android/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="textColor" type="color">#000000</item>
<item name="backgroundColor" type="color">#e8e8e8</item>
</resources>
6 changes: 0 additions & 6 deletions playground/android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,4 @@
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@color/backgroundColor</item>
</style>

<!--This is your application's default background color.
It will be visible when the app is first opened (while the splash layout is visible)
and when transitioning between a destination drawn under the StatusBar to
a destination drawn behind it-->
<item name="backgroundColor" type="color">#e8e8e8</item>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "light"
}
],
"color" : {
"platform" : "ios",
"reference" : "darkTextColor"
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.180",
"green" : "0.160",
"red" : "0.730"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading

0 comments on commit 1db86ce

Please sign in to comment.