From 0b07d89d8f0ace3d4655b7df46af3de12b5a0bdf Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Tue, 5 Jan 2021 19:05:11 -0800 Subject: [PATCH] feat(android): fetchSemanticColor() support dynamic light/dark change --- .../titanium/util/TiColorHelper.java | 31 ++++++++++-- .../extensions/ti/ui/semanticColor.js | 47 ++++++++++--------- common/lib/color.js | 2 +- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/android/titanium/src/java/org/appcelerator/titanium/util/TiColorHelper.java b/android/titanium/src/java/org/appcelerator/titanium/util/TiColorHelper.java index 679d35dc297..bbe28b73a90 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/util/TiColorHelper.java +++ b/android/titanium/src/java/org/appcelerator/titanium/util/TiColorHelper.java @@ -15,6 +15,7 @@ import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.TiApplication; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import androidx.annotation.ColorInt; @@ -91,6 +92,25 @@ public static int parseColor(String value) Math.round(Float.valueOf(m.group(2)) * 255f), Math.round(Float.valueOf(m.group(3)) * 255f)); } + // Check if this a "semantic.colors.json" generated string from our common "ti.ui.js" script. + // Example: "ti.semantic.color:dark=;light=" + final String TI_SEMANTIC_COLOR_PREFIX = "ti.semantic.color:"; + if (value.startsWith(TI_SEMANTIC_COLOR_PREFIX)) { + String themePrefix = "light="; + Configuration config = TiApplication.getInstance().getResources().getConfiguration(); + if ((config.uiMode & Configuration.UI_MODE_NIGHT_YES) != 0) { + themePrefix = "dark="; + } + String[] stringArray = value.substring(TI_SEMANTIC_COLOR_PREFIX.length()).split(";"); + for (String nextString : stringArray) { + if (nextString.startsWith(themePrefix)) { + return TiColorHelper.parseColor(nextString.substring(themePrefix.length())); + } + } + Log.e(TAG, "Cannot find named color: " + value); + return Color.TRANSPARENT; + } + // Ti.Android.R.color or Ti.App.Android.R.color resource (by name) if (TiColorHelper.hasColorResource(value)) { try { @@ -125,14 +145,19 @@ public static int parseColor(String value) public static boolean hasColorResource(String colorName) { - return TiRHelper.hasResource("color." + colorName); + return getColorResourceId(colorName) != 0; + } + + public static int getColorResourceId(String colorName) + { + TiApplication app = TiApplication.getInstance(); + return app.getResources().getIdentifier(colorName, "color", app.getPackageName()); } public static @ColorInt int getColorResource(String colorName) throws TiRHelper.ResourceNotFoundException, Resources.NotFoundException { - int colorResId = TiRHelper.getResource("color." + colorName); - // Now we need to convert it! + int colorResId = getColorResourceId(colorName); return ContextCompat.getColor(TiApplication.getInstance(), colorResId); } diff --git a/common/Resources/ti.internal/extensions/ti/ui/semanticColor.js b/common/Resources/ti.internal/extensions/ti/ui/semanticColor.js index 1f4051b9a69..eacfbb0b1ba 100644 --- a/common/Resources/ti.internal/extensions/ti/ui/semanticColor.js +++ b/common/Resources/ti.internal/extensions/ti/ui/semanticColor.js @@ -63,40 +63,45 @@ if (!isIOS13Plus && !isMACOSXCatalinaPlus) { let colorset; UI.fetchSemanticColor = function fetchSemanticColor (colorName) { + // Load all semantic colors from JSON if not done already. + // Do so via require() in case this file was changed while running LiveView. if (!colorset) { + const colorsetFileName = 'semantic.colors.json'; try { - const colorsetFile = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, 'semantic.colors.json'); + const colorsetFile = Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, colorsetFileName); if (colorsetFile.exists()) { - colorset = JSON.parse(colorsetFile.read().text); + // eslint-disable-next-line security/detect-non-literal-require + colorset = require(`/${colorsetFileName}`); } } catch (error) { - // We should probably throw an Error here (or return a fallback color!) - console.error('Failed to load colors file \'semantic.colors.json\''); + console.error(`Failed to load colors file '${colorsetFileName}'`); return Color.fallback().toHex(); } } try { - if (!colorset[colorName]) { - if (OS_ANDROID) { - // if it's not in the semantic colors and we're on Android, it may be a Ti.Android.R.color value - const systemColorId = Ti.Android.R.color[colorName]; - if (systemColorId) { - const resourceColor = Ti.UI.Android.getColorResource(systemColorId); - if (resourceColor) { - return resourceColor.toHex(); - } + if (OS_ANDROID) { + // On Android, use custom string references to be handled by "TiColorHelper.java". + if (colorset[colorName]) { + // Add all theme colors to a single string. + // Example: "ti.semantic.color:dark=;light=" + let stringResult = 'ti.semantic.color:'; + for (const colorType in colorset[colorName]) { + const colorObj = Color.fromSemanticColorsEntry(colorset[colorName][colorType]); + stringResult += `${colorType}=${colorObj.toRGBAString()};`; } + return stringResult; + } else if (Ti.Android.R.color[colorName]) { + // We're referencing a native "res" color entry. + return `@color/${colorName}`; } - return Color.fallback().toHex(); + } else if (colorset[colorName]) { + // Return the raw color string value from the "semantic.colors.json". + // Use the more exact rgba function over 8-char ARGB hex. Hard to convert things like 75% alpha properly. + const entry = colorset[colorName][UI.semanticColorType]; + const colorObj = Color.fromSemanticColorsEntry(entry); + return colorObj.toRGBAString(); } - - const entry = colorset[colorName][UI.semanticColorType]; - const colorObj = Color.fromSemanticColorsEntry(entry); - // For now, return a string on iOS < 13, Android so we can pass the result directly to the UI property we want to set - // Otherwise we need to modify the Android APIs to accept fake/real Ti.UI.Color instances and convert it to it's own internal - // Color representation - return colorObj.toRGBAString(); // If there's an entry, use the more exact rgba function over 8-char ARGB hex. Hard to convert things like 75% alpha properly. } catch (error) { console.error(`Failed to lookup color for ${colorName}`); } diff --git a/common/lib/color.js b/common/lib/color.js index b9afd545899..3ec0c420b03 100644 --- a/common/lib/color.js +++ b/common/lib/color.js @@ -96,7 +96,7 @@ class Color { /** * Converts this color to an rgba expression. This expression is more consistent across platforms. - * (whereas iOS/Android differ in expecttaiosn for hex strings.) + * (whereas iOS/Android differ in expectations for hex strings.) * @returns {string} */ toRGBAString() {