Skip to content

Commit 5c9a217

Browse files
vchimevmanoldonev
authored andcommitted
feat(dark-mode): add system appearance property, event and CSS classes (#7887)
1 parent d7d7ebf commit 5c9a217

16 files changed

Lines changed: 337 additions & 54 deletions

File tree

‎tns-core-modules/application/application-common.ts‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
} from "./application";
4343

4444
import { CLASS_PREFIX, pushToRootViewCssClasses, removeFromRootViewCssClasses } from "../css/system-classes";
45-
import { DeviceOrientation } from "../ui/enums/enums";
45+
import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums";
4646

4747
export { UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData };
4848

@@ -55,13 +55,19 @@ export const lowMemoryEvent = "lowMemory";
5555
export const uncaughtErrorEvent = "uncaughtError";
5656
export const discardedErrorEvent = "discardedError";
5757
export const orientationChangedEvent = "orientationChanged";
58+
export const systemAppearanceChangedEvent = "systemAppearanceChanged";
5859

5960
const ORIENTATION_CSS_CLASSES = [
6061
`${CLASS_PREFIX}${DeviceOrientation.portrait}`,
6162
`${CLASS_PREFIX}${DeviceOrientation.landscape}`,
6263
`${CLASS_PREFIX}${DeviceOrientation.unknown}`
6364
];
6465

66+
const SYSTEM_APPEARANCE_CSS_CLASSES = [
67+
`${CLASS_PREFIX}${SystemAppearance.light}`,
68+
`${CLASS_PREFIX}${SystemAppearance.dark}`
69+
];
70+
6571
let cssFile: string = "./app.css";
6672

6773
let resources: any = {};
@@ -126,17 +132,30 @@ export function loadAppCss(): void {
126132
}
127133
}
128134

135+
function applyCssClass(rootView: View, cssClass: string) {
136+
pushToRootViewCssClasses(cssClass);
137+
rootView.cssClasses.add(cssClass);
138+
}
139+
140+
function removeCssClass(rootView: View, cssClass: string) {
141+
removeFromRootViewCssClasses(cssClass);
142+
rootView.cssClasses.delete(cssClass);
143+
}
144+
129145
export function orientationChanged(rootView: View, newOrientation: "portrait" | "landscape" | "unknown"): void {
130146
const newOrientationCssClass = `${CLASS_PREFIX}${newOrientation}`;
131147
if (!rootView.cssClasses.has(newOrientationCssClass)) {
132-
const removeCssClass = (c: string) => {
133-
removeFromRootViewCssClasses(c);
134-
rootView.cssClasses.delete(c);
135-
};
136-
137-
ORIENTATION_CSS_CLASSES.forEach(c => removeCssClass(c));
138-
pushToRootViewCssClasses(newOrientationCssClass);
139-
rootView.cssClasses.add(newOrientationCssClass);
148+
ORIENTATION_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass));
149+
applyCssClass(rootView, newOrientationCssClass);
150+
rootView._onCssStateChange();
151+
}
152+
}
153+
154+
export function systemAppearanceChanged(rootView: View, newSystemAppearance: "dark" | "light"): void {
155+
const newSystemAppearanceCssClass = `${CLASS_PREFIX}${newSystemAppearance}`;
156+
if (!rootView.cssClasses.has(newSystemAppearanceCssClass)) {
157+
SYSTEM_APPEARANCE_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass));
158+
applyCssClass(rootView, newSystemAppearanceCssClass);
140159
rootView._onCssStateChange();
141160
}
142161
}

‎tns-core-modules/application/application.android.ts‎

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
AndroidApplication as AndroidApplicationDefinition,
1010
ApplicationEventData,
1111
CssChangedEventData,
12-
OrientationChangedEventData
12+
OrientationChangedEventData,
13+
SystemAppearanceChangedEventData
1314
} from ".";
1415

1516
import {
1617
displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, on,
17-
orientationChanged, orientationChangedEvent, setApplication, suspendEvent
18+
orientationChanged, orientationChangedEvent, setApplication, suspendEvent,
19+
systemAppearanceChanged, systemAppearanceChangedEvent
1820
} from "./application-common";
1921

2022
import { profile } from "../profiling";
@@ -51,6 +53,7 @@ export class AndroidApplication extends Observable implements AndroidApplication
5153
public static activityRequestPermissionsEvent = ActivityRequestPermissions;
5254

5355
private _orientation: "portrait" | "landscape" | "unknown";
56+
private _systemAppearance: "light" | "dark";
5457
public paused: boolean;
5558
public nativeApp: android.app.Application;
5659
public context: android.content.Context;
@@ -93,9 +96,8 @@ export class AndroidApplication extends Observable implements AndroidApplication
9396
if (!this._orientation) {
9497
const resources = this.context.getResources();
9598
const configuration = <android.content.res.Configuration>resources.getConfiguration();
96-
const orientation = configuration.orientation;
9799

98-
this._orientation = getOrientationValue(orientation);
100+
this._orientation = getOrientationValue(configuration);
99101
}
100102

101103
return this._orientation;
@@ -105,6 +107,21 @@ export class AndroidApplication extends Observable implements AndroidApplication
105107
this._orientation = value;
106108
}
107109

110+
get systemAppearance(): "light" | "dark" {
111+
if (!this._systemAppearance) {
112+
const resources = this.context.getResources();
113+
const configuration = <android.content.res.Configuration>resources.getConfiguration();
114+
115+
this._systemAppearance = getSystemAppearanceValue(configuration);
116+
}
117+
118+
return this._systemAppearance;
119+
}
120+
121+
set systemAppearance(value: "light" | "dark") {
122+
this._systemAppearance = value;
123+
}
124+
108125
public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) {
109126
ensureBroadCastReceiverClass();
110127
const that = this;
@@ -131,6 +148,7 @@ export class AndroidApplication extends Observable implements AndroidApplication
131148
}
132149
}
133150
}
151+
134152
export interface AndroidApplication {
135153
on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any);
136154
on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any);
@@ -252,12 +270,9 @@ export function orientation(): "portrait" | "landscape" | "unknown" {
252270
return androidApp.orientation;
253271
}
254272

255-
on(orientationChangedEvent, (args: OrientationChangedEventData) => {
256-
const rootView = getRootView();
257-
if (rootView) {
258-
orientationChanged(rootView, args.newValue);
259-
}
260-
});
273+
export function systemAppearance(): "dark" | "light" {
274+
return androidApp.systemAppearance;
275+
}
261276

262277
global.__onLiveSync = function __onLiveSync(context?: ModuleContext) {
263278
if (androidApp && androidApp.paused) {
@@ -268,7 +283,9 @@ global.__onLiveSync = function __onLiveSync(context?: ModuleContext) {
268283
livesync(rootView, context);
269284
};
270285

271-
function getOrientationValue(orientation: number): "portrait" | "landscape" | "unknown" {
286+
function getOrientationValue(configuration: android.content.res.Configuration): "portrait" | "landscape" | "unknown" {
287+
const orientation = configuration.orientation;
288+
272289
switch (orientation) {
273290
case android.content.res.Configuration.ORIENTATION_LANDSCAPE:
274291
return "landscape";
@@ -279,6 +296,19 @@ function getOrientationValue(orientation: number): "portrait" | "landscape" | "u
279296
}
280297
}
281298

299+
// https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#configuration_changes
300+
function getSystemAppearanceValue(configuration: android.content.res.Configuration): "dark" | "light" {
301+
const systemAppearance = configuration.uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK;
302+
303+
switch (systemAppearance) {
304+
case android.content.res.Configuration.UI_MODE_NIGHT_YES:
305+
return "dark";
306+
case android.content.res.Configuration.UI_MODE_NIGHT_NO:
307+
case android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED:
308+
return "light";
309+
}
310+
}
311+
282312
function initLifecycleCallbacks() {
283313
const setThemeOnLaunch = profile("setThemeOnLaunch", (activity: androidx.appcompat.app.AppCompatActivity) => {
284314
// Set app theme after launch screen was used during startup
@@ -380,19 +410,35 @@ function initComponentCallbacks() {
380410
// TODO: This is skipped for now, test carefully for OutOfMemory exceptions
381411
}),
382412

383-
onConfigurationChanged: profile("onConfigurationChanged", function (newConfig: android.content.res.Configuration) {
384-
const newConfigOrientation = newConfig.orientation;
385-
const newOrientation = getOrientationValue(newConfigOrientation);
413+
onConfigurationChanged: profile("onConfigurationChanged", function (newConfiguration: android.content.res.Configuration) {
414+
const newOrientation = getOrientationValue(newConfiguration);
386415

387416
if (androidApp.orientation !== newOrientation) {
388417
androidApp.orientation = newOrientation;
418+
orientationChanged(getRootView(), newOrientation);
389419

390420
notify(<OrientationChangedEventData>{
391421
eventName: orientationChangedEvent,
392422
android: androidApp.nativeApp,
393423
newValue: androidApp.orientation,
394424
object: androidApp
395425
});
426+
427+
return;
428+
}
429+
430+
const newSystemAppearance = getSystemAppearanceValue(newConfiguration);
431+
432+
if (androidApp.systemAppearance !== newSystemAppearance) {
433+
androidApp.systemAppearance = newSystemAppearance;
434+
systemAppearanceChanged(getRootView(), newSystemAppearance);
435+
436+
notify(<SystemAppearanceChangedEventData>{
437+
eventName: systemAppearanceChangedEvent,
438+
android: androidApp.nativeApp,
439+
newValue: androidApp.systemAppearance,
440+
object: androidApp
441+
});
396442
}
397443
})
398444
});

‎tns-core-modules/application/application.d.ts‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ export interface OrientationChangedEventData extends ApplicationEventData {
101101
newValue: "portrait" | "landscape" | "unknown";
102102
}
103103

104+
/**
105+
* Event data containing information for system appearance changed event.
106+
*/
107+
export interface SystemAppearanceChangedEventData extends ApplicationEventData {
108+
/**
109+
* New system appearance value.
110+
*/
111+
newValue: "light" | "dark";
112+
}
113+
104114
/**
105115
* Event data containing information about unhandled application errors.
106116
*/
@@ -281,16 +291,29 @@ export function on(event: "uncaughtError", callback: (args: UnhandledErrorEventD
281291
export function on(event: "discardedError", callback: (args: DiscardedErrorEventData) => void, thisArg?: any);
282292

283293
/**
284-
* This event is raised the orientation of the current device has changed.
294+
* This event is raised when the orientation of the application changes.
285295
*/
286296
export function on(event: "orientationChanged", callback: (args: OrientationChangedEventData) => void, thisArg?: any);
287297

298+
/**
299+
* This event is raised when the operating system appearance changes
300+
* between light and dark theme (for Android);
301+
* between light and dark mode (for iOS) and vice versa.
302+
*/
303+
export function on(event: "systemAppearanceChanged", callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any);
304+
288305
/**
289306
* Gets the orientation of the application.
290307
* Available values: "portrait", "landscape", "unknown".
291308
*/
292309
export function orientation(): "portrait" | "landscape" | "unknown";
293310

311+
/**
312+
* Gets the operating system appearance.
313+
* Available values: "dark", "light".
314+
*/
315+
export function systemAppearance(): "dark" | "light";
316+
294317
/**
295318
* This is the Android-specific application object instance.
296319
* Encapsulates methods and properties specific to the Android platform.
@@ -425,6 +448,12 @@ export class AndroidApplication extends Observable {
425448
*/
426449
orientation: "portrait" | "landscape" | "unknown";
427450

451+
/**
452+
* Gets the system appearance.
453+
* Available values: "dark", "light".
454+
*/
455+
systemAppearance: "dark" | "light";
456+
428457
/**
429458
* The name of the application package.
430459
*/
@@ -604,6 +633,12 @@ export interface iOSApplication {
604633
*/
605634
orientation: "portrait" | "landscape" | "unknown";
606635

636+
/**
637+
* Gets the system appearance.
638+
* Available values: "dark", "light".
639+
*/
640+
systemAppearance: "dark" | "light";
641+
607642
/**
608643
* The [UIApplication](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html).
609644
*/
@@ -627,7 +662,9 @@ export interface iOSApplication {
627662
removeNotificationObserver(observer: any, notificationName: string): void;
628663
}
629664

630-
/* tslint:disable */
665+
/**
666+
* @deprecated
667+
*/
631668
export interface RootViewControllerImpl {
632669
contentController: any;
633670
}

0 commit comments

Comments
 (0)