Skip to content

Commit

Permalink
feat: cross-platform light/dark mode API (#11457)
Browse files Browse the repository at this point in the history
* fix(ios): fetchSemanticColor returned object on iOS 13+

* fix(ios): try to make fetchSemanticColor more sane

* fix: return fake TiColor on Android/older iOS

* feat(android): semantic colors and night mode

- generate color resources from semantic.colors.json
- add Ti.UI.Android.nightModeStatus API
- add Ti.UI.Android.MODE_NIGHT_* constants

refs TIMOB-27501

* test: update assertions for fetchSemanticColor

* docs: add Ti.UI.Color

* fix(android): report dark mode on Android via Ti.UI.demanticColorType

* docs: update semantic color info

* refactor(android): move to common API for determining dark/light mode

* docs(android): remove night mode related docs

* docs(ios): deprecate ios namespaced dark/light mode stuff

* docs: deprecate semanticColorType stuff, move to userInterfaceStyle

* fix(ios): deprecate ios namespaced userInterfaceStyle

* fix(ios): move to cross-platform userInterfaceStyle API

* refactor: use cross-platform API for userInterfaceStyle

* fix(ios): move fetchSemnaticColor to UIModule

* fix: don't override Ti.UI.fetchSemanticColor on iOS 13+

* docs: add note that Color objects not supported by Android

* docs: update any color property accepting Ti.UI.Color object on iOS

* feat(ios): add userInterfaceStyle event to Ti.UI

* test(android): remove android-specific night mode tests

* fix: oops invert condition for overriding Ti.UI.fetchSemnaticColor

* feat(android): add userInterfaceStyle event to Ti.UI

* test: add tests for USER_INTERFACE constants

* docs: document Ti.UI userInterfaceStyle event

* fix(android): get userInterfaceStyle event working

* fix: revert back to returning string color for android/iOS < 13

* test: modify to expect String on Android/iOS < 13

* feat(ios): support use of named system colors baked into ios

* docs: mark Ti.UI.Color as ios-only, link to system color docs

* fix(android): use lowercase userinterfacestyle event name

* fix(ios): use lowercase userinterfacestyle event name

* docs: use lowercase userinterfacestyle event name

* docs: address review comment

* build(android): address review comment, write file with ti prefix

* fix(android): manage userinterfacestyle receiver in UIModule constructor

Co-authored-by: Sergey Volkov <s.volkov@netris.ru>
Co-authored-by: ssekhri <ssekhri@axway.com>
  • Loading branch information
3 people committed May 8, 2020
1 parent 271ebb4 commit 28eba34
Show file tree
Hide file tree
Showing 49 changed files with 906 additions and 199 deletions.
100 changes: 99 additions & 1 deletion android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -2303,7 +2303,10 @@ AndroidBuilder.prototype.generateAppProject = async function generateAppProject(
this.generateI18N(),

// Generate a "res/values" styles XML file if a custom theme was assigned in app's "AndroidManifest.xml".
this.generateTheme()
this.generateTheme(),

// Generate "semantic.colors.xml" in "res/values" and "res/values-night"
this.generateSemanticColors()
]);

// Generate an "AndroidManifest.xml" for the app and copy in any custom manifest settings from "tiapp.xml".
Expand Down Expand Up @@ -3314,6 +3317,101 @@ AndroidBuilder.prototype.generateI18N = async function generateI18N() {
}
};

AndroidBuilder.prototype.generateSemanticColors = async function generateSemanticColors() {
this.logger.info(__('Generating semantic colors resources'));
const _t = this;
const xmlFileName = 'ti.semantic.colors.xml';
const valuesDirPath = path.join(this.buildAppMainResDir, 'values');
const valuesNightDirPath = path.join(this.buildAppMainResDir, 'values-night');
await fs.ensureDir(valuesDirPath);
await fs.ensureDir(valuesNightDirPath);
const destLight = path.join(valuesDirPath, xmlFileName);
const destNight = path.join(valuesNightDirPath, xmlFileName);

let colorsFile = path.join(this.projectDir, 'Resources', 'android', 'semantic.colors.json');

if (!fs.existsSync(colorsFile)) {
// Fallback to root of Resources folder for Classic applications
colorsFile = path.join(this.projectDir, 'Resources', 'semantic.colors.json');
}

if (!fs.existsSync(colorsFile)) {
this.logger.debug(__('Skipping colorset generation as "semantic.colors.json" file does not exist'));
return;
}

const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;

function hexToRgb(hex) {
let alphaHex = 'ff';
let color = hex;
if (hex.color) {
color = hex.color;
let alpha = Math.round(255 * parseFloat(hex.alpha) / 100);
if (alpha <= 255) {
alphaHex = alpha.toString(16);
if (alpha < 16) {
alphaHex = '0' + alphaHex;
}
}
}
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
color = color.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);

var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
if (alphaHex === 'ff') {
return `#${result[1]}${result[2]}${result[3]}`;
} else {
return `#${alphaHex}${result[1]}${result[2]}${result[3]}`;
}
}

function appendToXml(dom, root, color, colorValue) {
const appnameNode = dom.createElement('color');

appnameNode.setAttribute('name', `${color}`);
appnameNode.appendChild(dom.createTextNode(hexToRgb(colorValue)));
root.appendChild(dom.createTextNode('\n\t'));
root.appendChild(appnameNode);
}

function writeXml(dom, dest, mode) {
if (fs.existsSync(dest)) {
_t.logger.debug(__('Merging %s semantic colors => %s', mode.cyan, dest.cyan));
} else {
_t.logger.debug(__('Writing %s semantic colors => %s', mode.cyan, dest.cyan));
}
return fs.writeFile(dest, '<?xml version="1.0" encoding="UTF-8"?>\n' + dom.documentElement.toString());
}

const colors = fs.readJSONSync(colorsFile);
const domLight = new DOMParser().parseFromString('<resources/>', 'text/xml');
const domNight = new DOMParser().parseFromString('<resources/>', 'text/xml');

const rootLight = domLight.documentElement;
const rootNight = domNight.documentElement;

for (const [ color, colorValue ] of Object.entries(colors)) {
if (!colorValue.light) {
this.logger.warn(`Skipping ${color} as it does not include a light value`);
continue;
}

if (!colorValue.dark) {
this.logger.warn(`Skipping ${color} as it does not include a dark value`);
continue;
}

appendToXml(domLight, rootLight, color, colorValue.light);
appendToXml(domNight, rootNight, color, colorValue.dark);
}

return Promise.all([
writeXml(domLight, destLight, 'light'),
writeXml(domNight, destNight, 'night')
]);
};

AndroidBuilder.prototype.generateTheme = async function generateTheme() {
// Log the theme XML file we're about to generate.
const valuesDirPath = path.join(this.buildAppMainResDir, 'values');
Expand Down
68 changes: 67 additions & 1 deletion android/modules/ui/src/java/ti/modules/titanium/ui/UIModule.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2009-2016 by Appcelerator, Inc. All Rights Reserved.
* Copyright (c) 2009-2020 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
package ti.modules.titanium.ui;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollModule;
import org.appcelerator.kroll.KrollProxy;
import org.appcelerator.kroll.KrollRuntime;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.titanium.TiApplication;
Expand All @@ -22,6 +24,11 @@
import org.appcelerator.titanium.util.TiUIHelper;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
Expand Down Expand Up @@ -377,11 +384,36 @@ public class UIModule extends KrollModule
@Kroll.constant
public static final int HIDDEN_BEHAVIOR_INVISIBLE = View.INVISIBLE;

@Kroll.constant
public static final int USER_INTERFACE_STYLE_LIGHT = Configuration.UI_MODE_NIGHT_NO;
@Kroll.constant
public static final int USER_INTERFACE_STYLE_DARK = Configuration.UI_MODE_NIGHT_YES;
@Kroll.constant
public static final int USER_INTERFACE_STYLE_UNSPECIFIED = Configuration.UI_MODE_NIGHT_UNDEFINED;

protected static final int MSG_LAST_ID = KrollProxy.MSG_LAST_ID + 101;

public UIModule()
{
super();

// Register the module's broadcast receiver.
final UIModule.Receiver broadcastReceiver = new UIModule.Receiver(this);
TiApplication.getInstance().registerReceiver(broadcastReceiver,
new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));

// Set up a listener to be invoked when the JavaScript runtime is about to be terminated/disposed.
KrollRuntime.addOnDisposingListener(new KrollRuntime.OnDisposingListener() {
@Override
public void onDisposing(KrollRuntime runtime)
{
// Remove this listener from the runtime's static collection.
KrollRuntime.removeOnDisposingListener(this);

// Unregister this module's broadcast receviers.
TiApplication.getInstance().unregisterReceiver(broadcastReceiver);
}
});
}

@Kroll.setProperty(runOnUiThread = true)
Expand Down Expand Up @@ -483,9 +515,43 @@ protected void doSetOrientation(int tiOrientationMode)
}
}

@Kroll.getProperty
public int getUserInterfaceStyle()
{
return TiApplication.getInstance().getApplicationContext().getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
}

@Override
public String getApiName()
{
return "Ti.UI";
}

private class Receiver extends BroadcastReceiver
{
private UIModule module;
private int lastEmittedStyle;

public Receiver(UIModule module)
{
super();
this.module = module;
lastEmittedStyle = this.module.getUserInterfaceStyle();
}

@Override
public void onReceive(Context context, Intent intent)
{
int currentMode = this.module.getUserInterfaceStyle();
if (currentMode == lastEmittedStyle) {
return;
}
lastEmittedStyle = currentMode;

KrollDict event = new KrollDict();
event.put(TiC.PROPERTY_VALUE, lastEmittedStyle);
this.module.fireEvent(TiC.EVENT_USER_INTERFACE_STYLE, event);
}
}
}
9 changes: 7 additions & 2 deletions android/titanium/src/java/org/appcelerator/titanium/TiC.java
Original file line number Diff line number Diff line change
Expand Up @@ -697,12 +697,17 @@ public class TiC
/**
* @module.api
*/
public static final String EVENT_USER_LEAVE_HINT = "userleavehint";
public static final String EVENT_USER_INTERACTION = "userinteraction";

/**
* @module.api
*/
public static final String EVENT_USER_INTERACTION = "userinteraction";
public static final String EVENT_USER_INTERFACE_STYLE = "userinterfacestyle";

/**
* @module.api
*/
public static final String EVENT_USER_LEAVE_HINT = "userleavehint";

/**
* @module.api
Expand Down
15 changes: 14 additions & 1 deletion apidoc/Titanium/App/iOS/iOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ properties:
permission: read-only
osver: {ios: {min: "13.0"}}
since: "8.2.0"
deprecated:
since: "9.1.0"
notes: Use <Titanium.UI.USER_INTERFACE_STYLE_UNSPECIFIED> instead, which is supported cross-platform.

- name: USER_INTERFACE_STYLE_LIGHT
summary: A light interface style.
Expand All @@ -501,6 +504,9 @@ properties:
permission: read-only
osver: {ios: {min: "13.0"}}
since: "8.2.0"
deprecated:
since: "9.1.0"
notes: Use <Titanium.UI.USER_INTERFACE_STYLE_LIGHT> instead, which is supported cross-platform.

- name: USER_INTERFACE_STYLE_DARK
summary: A dark interface style.
Expand All @@ -509,6 +515,9 @@ properties:
permission: read-only
osver: {ios: {min: "13.0"}}
since: "8.2.0"
deprecated:
since: "9.1.0"
notes: Use <Titanium.UI.USER_INTERFACE_STYLE_DARK> instead, which is supported cross-platform.

- name: UTTYPE_TEXT
summary: |
Expand Down Expand Up @@ -829,10 +838,14 @@ properties:
description: |
Use this property to determine whether your interface should be configured with a dark or light appearance.
The default value of this trait is set to the corresponding appearance setting on the user's device.
type: Array<String>
type: Number
permission: read-only
osver: {ios: {min: "13.0"}}
since: "8.2.0"
constants: Titanium.App.iOS.USER_INTERFACE_STYLE_*
deprecated:
since: "9.1.0"
notes: Use <Titanium.UI.userInterfaceStyle> instead, which is supported cross-platform.

events:
- name: notification
Expand Down
17 changes: 9 additions & 8 deletions apidoc/Titanium/UI/ActivityIndicator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ properties:
Color of the message text, as a color name or hex triplet.
description: |
For information about color values, see the "Colors" section of <Titanium.UI>.
type: String
type: [ String, Titanium.UI.Color ]

- name: font
summary: Font used for the message text.
Expand All @@ -83,6 +83,14 @@ properties:
type: String
constants: Titanium.UI.SIZE

- name: indicatorColor
summary: Color of the animated indicator.
description: For information about color values, see the "Colors" section of <Titanium.UI>.
since: "2.1.0"
type: [ String, Titanium.UI.Color ]
default: "white"
platforms: [iphone,ipad,android]

- name: left
summary: Left position of the view.
description: |
Expand Down Expand Up @@ -118,13 +126,6 @@ properties:
constants: Titanium.UI.ActivityIndicatorStyle.*
default: <Titanium.UI.ActivityIndicatorStyle.PLAIN>

- name: indicatorColor
summary: Color of the animated indicator.
since: "2.1.0"
type: String
default: white
platforms: [iphone,ipad,android]

- name: top
summary: Top position of the view.
description: |
Expand Down
3 changes: 2 additions & 1 deletion apidoc/Titanium/UI/AlertDialog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,8 @@ properties:
description: |
This property is a direct correspondant of the `tintColor` property of
UIView on iOS. For a dialog, it will tint the color of it's buttons.
type: [String]
For information about color values, see the "Colors" section of <Titanium.UI>.
type: [ String, Titanium.UI.Color ]
since: "6.2.0"
osver: {ios: {min: "8.0"}}
platforms: [iphone, ipad]
Expand Down
5 changes: 2 additions & 3 deletions apidoc/Titanium/UI/Animation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ properties:
or hex triplet.
description: |
For information about color values, see the "Colors" section of <Titanium.UI>.
type: String
type: [String, Titanium.UI.Color]
platforms: [android, iphone, ipad]

- name: bottom
Expand All @@ -95,7 +95,7 @@ properties:
Value of the `color` property at the end of the animation, as a color name or hex triplet.
description: |
For information about color values, see the "Colors" section of <Titanium.UI>.
type: String
type: [String, Titanium.UI.Color]
platforms: [android, iphone, ipad]
since: { android: "9.1.0" }

Expand Down Expand Up @@ -183,7 +183,6 @@ properties:
3D transforms are only supported on iOS.
type: [Titanium.UI.Matrix2D, Titanium.UI.Matrix3D]


- name: transition
summary: Transition type to use during a transition animation.
description: |
Expand Down
2 changes: 1 addition & 1 deletion apidoc/Titanium/UI/Attribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ properties:
* <Titanium.UI.ATTRIBUTE_LINE_BREAK_BY_TRUNCATING_MIDDLE>
These can also be combined the same way as the underline styles.
type: Object
type: [String, Number, Titanium.UI.Color, Object ]
constants: [ Titanium.UI.ATTRIBUTE_UNDERLINE_STYLE_*,
Titanium.UI.ATTRIBUTE_WRITING_DIRECTION_*,
Titanium.UI.ATTRIBUTE_LETTERPRESS_STYLE ]
Expand Down
Loading

0 comments on commit 28eba34

Please sign in to comment.