From d81be715d23ed3a4dc4311a107e49074d000b4f9 Mon Sep 17 00:00:00 2001 From: Vasil Chimev Date: Wed, 2 Oct 2019 15:30:45 +0300 Subject: [PATCH] feat(mode-night): apply ns-dark|ns-light class to root view at app launch --- .../application/application-common.ts | 20 ++++- .../application/application.android.ts | 55 +++++++++++- tns-core-modules/application/application.d.ts | 44 ++++++---- .../application/application.ios.ts | 83 ++++++++++++------- tns-core-modules/ui/enums/enums.ts | 2 +- tns-core-modules/ui/frame/frame.android.ts | 1 + 6 files changed, 153 insertions(+), 52 deletions(-) diff --git a/tns-core-modules/application/application-common.ts b/tns-core-modules/application/application-common.ts index 840ad1c41..a967bb5ef 100644 --- a/tns-core-modules/application/application-common.ts +++ b/tns-core-modules/application/application-common.ts @@ -42,7 +42,7 @@ import { } from "./application"; import { CLASS_PREFIX, pushToRootViewCssClasses, removeFromRootViewCssClasses } from "../css/system-classes"; -import { DeviceOrientation } from "../ui/enums/enums"; +import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums"; export { UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData }; @@ -55,6 +55,7 @@ export const lowMemoryEvent = "lowMemory"; export const uncaughtErrorEvent = "uncaughtError"; export const discardedErrorEvent = "discardedError"; export const orientationChangedEvent = "orientationChanged"; +export const systemAppearanceChangedEvent = "systemAppearanceChanged"; const ORIENTATION_CSS_CLASSES = [ `${CLASS_PREFIX}${DeviceOrientation.portrait}`, @@ -62,6 +63,11 @@ const ORIENTATION_CSS_CLASSES = [ `${CLASS_PREFIX}${DeviceOrientation.unknown}` ]; +const SYSTEM_APPEARANCE_CSS_CLASSES = [ + `${CLASS_PREFIX}${SystemAppearance.light}`, + `${CLASS_PREFIX}${SystemAppearance.dark}` +]; + let cssFile: string = "./app.css"; let resources: any = {}; @@ -126,13 +132,13 @@ export function loadAppCss(): void { } } -export function applyCssClass(rootView: View, cssClass: string) { +function applyCssClass(rootView: View, cssClass: string) { pushToRootViewCssClasses(cssClass); rootView.cssClasses.add(cssClass); rootView._onCssStateChange(); } -export function removeCssClass(rootView: View, cssClass: string) { +function removeCssClass(rootView: View, cssClass: string) { removeFromRootViewCssClasses(cssClass); rootView.cssClasses.delete(cssClass); } @@ -145,6 +151,14 @@ export function orientationChanged(rootView: View, newOrientation: "portrait" | } } +export function systemAppearanceChanged(rootView: View, newSystemAppearance: "dark" | "light"): void { + const newSystemAppearanceCssClass = `${CLASS_PREFIX}${newSystemAppearance}`; + if (!rootView.cssClasses.has(newSystemAppearanceCssClass)) { + SYSTEM_APPEARANCE_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass)); + applyCssClass(rootView, newSystemAppearanceCssClass); + } +} + global.__onUncaughtError = function (error: NativeScriptError) { events.notify({ eventName: uncaughtErrorEvent, object: app, android: error, ios: error, error: error }); }; diff --git a/tns-core-modules/application/application.android.ts b/tns-core-modules/application/application.android.ts index 25e04594b..dc93842be 100644 --- a/tns-core-modules/application/application.android.ts +++ b/tns-core-modules/application/application.android.ts @@ -9,12 +9,14 @@ import { AndroidApplication as AndroidApplicationDefinition, ApplicationEventData, CssChangedEventData, - OrientationChangedEventData + OrientationChangedEventData, + SystemAppearanceChangedEventData } from "."; import { displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, on, - orientationChanged, orientationChangedEvent, setApplication, suspendEvent + orientationChanged, orientationChangedEvent, setApplication, suspendEvent, + systemAppearanceChanged, systemAppearanceChangedEvent } from "./application-common"; import { profile } from "../profiling"; @@ -51,6 +53,7 @@ export class AndroidApplication extends Observable implements AndroidApplication public static activityRequestPermissionsEvent = ActivityRequestPermissions; private _orientation: "portrait" | "landscape" | "unknown"; + private _systemAppearance: "light" | "dark"; public paused: boolean; public nativeApp: android.app.Application; public context: android.content.Context; @@ -105,6 +108,22 @@ export class AndroidApplication extends Observable implements AndroidApplication this._orientation = value; } + get systemAppearance(): "light" | "dark" { + if (!this._systemAppearance) { + const resources = this.context.getResources(); + const configuration = resources.getConfiguration(); + const systemAppearance = configuration.uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK; + + this._systemAppearance = getSystemAppearanceValue(systemAppearance); + } + + return this._systemAppearance; + } + + set systemAppearance(value: "light" | "dark") { + this._systemAppearance = value; + } + public registerBroadcastReceiver(intentFilter: string, onReceiveCallback: (context: android.content.Context, intent: android.content.Intent) => void) { ensureBroadCastReceiverClass(); const that = this; @@ -131,6 +150,7 @@ export class AndroidApplication extends Observable implements AndroidApplication } } } + export interface AndroidApplication { on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any); on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any); @@ -259,6 +279,13 @@ on(orientationChangedEvent, (args: OrientationChangedEventData) => { } }); +on(systemAppearanceChangedEvent, (args: SystemAppearanceChangedEventData) => { + const rootView = getRootView(); + if (rootView) { + systemAppearanceChanged(rootView, args.newValue); + } +}); + global.__onLiveSync = function __onLiveSync(context?: ModuleContext) { if (androidApp && androidApp.paused) { return; @@ -279,6 +306,16 @@ function getOrientationValue(orientation: number): "portrait" | "landscape" | "u } } +function getSystemAppearanceValue(systemAppearance: number): "dark" | "light" { + switch (systemAppearance) { + case android.content.res.Configuration.UI_MODE_NIGHT_YES: + return "dark"; + case android.content.res.Configuration.UI_MODE_NIGHT_NO: + case android.content.res.Configuration.UI_MODE_NIGHT_UNDEFINED: + return "light"; + } +} + function initLifecycleCallbacks() { const setThemeOnLaunch = profile("setThemeOnLaunch", (activity: androidx.appcompat.app.AppCompatActivity) => { // Set app theme after launch screen was used during startup @@ -394,6 +431,20 @@ function initComponentCallbacks() { object: androidApp }); } + + const newConfigSystemAppearance = newConfig.uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK; + const newSystemAppearance = getSystemAppearanceValue(newConfigSystemAppearance); + + if (androidApp.systemAppearance !== newSystemAppearance) { + androidApp.systemAppearance = newSystemAppearance; + + notify({ + eventName: systemAppearanceChangedEvent, + android: androidApp.nativeApp, + newValue: androidApp.systemAppearance, + object: androidApp + }); + } }) }); diff --git a/tns-core-modules/application/application.d.ts b/tns-core-modules/application/application.d.ts index b606a7f5d..f177d1eb2 100644 --- a/tns-core-modules/application/application.d.ts +++ b/tns-core-modules/application/application.d.ts @@ -101,6 +101,16 @@ export interface OrientationChangedEventData extends ApplicationEventData { newValue: "portrait" | "landscape" | "unknown"; } +/** + * Event data containing information for system appearance changed event. + */ +export interface SystemAppearanceChangedEventData extends ApplicationEventData { + /** + * New system appearance value. + */ + newValue: "light" | "dark"; +} + /** * Event data containing information about unhandled application errors. */ @@ -281,30 +291,20 @@ export function on(event: "uncaughtError", callback: (args: UnhandledErrorEventD export function on(event: "discardedError", callback: (args: DiscardedErrorEventData) => void, thisArg?: any); /** - * This event is raised the orientation of the current device has changed. + * This event is raised when the orientation of the application changes. */ export function on(event: "orientationChanged", callback: (args: OrientationChangedEventData) => void, thisArg?: any); /** -<<<<<<< HEAD + * This event is raised when the system appearance changes. + */ +export function on(event: "systemAppearanceChanged", callback: (args: SystemAppearanceChangedEventData) => void, thisArg?: any); + +/** * Gets the orientation of the application. * Available values: "portrait", "landscape", "unknown". */ export function orientation(): "portrait" | "landscape" | "unknown"; -======= - * Appends new CSS class to the system classes and applies it to the root view. - * @param rootView - The root view of the application. - * @param cssClass - The CSS class to apply. - */ -export function applyCssClass(rootView: View, cssClass: string); - -/** - * Removes CSS class from the system classes and deletes it from the root view. - * @param rootView - The root view of the application. - * @param cssClass - The CSS class to delete. - */ -export function removeCssClass(rootView: View, cssClass: string); ->>>>>>> refactor(dark-mode): application module /** * This is the Android-specific application object instance. @@ -440,6 +440,12 @@ export class AndroidApplication extends Observable { */ orientation: "portrait" | "landscape" | "unknown"; + /** + * Gets the system appearance. + * Available values: "dark", "light". + */ + systemAppearance: "dark" | "light"; + /** * The name of the application package. */ @@ -619,6 +625,12 @@ export interface iOSApplication { */ orientation: "portrait" | "landscape" | "unknown"; + /** + * Gets the system appearance. + * Available values: "dark", "light". + */ + systemAppearance: "dark" | "light"; + /** * The [UIApplication](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html). */ diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts index 9f2ea0b48..1720a5f67 100644 --- a/tns-core-modules/application/application.ios.ts +++ b/tns-core-modules/application/application.ios.ts @@ -4,12 +4,14 @@ import { iOSApplication as IOSApplicationDefinition, LaunchEventData, LoadAppCSSEventData, - OrientationChangedEventData + OrientationChangedEventData, + SystemAppearanceChangedEventData } from "."; import { - applyCssClass, displayedEvent, exitEvent, getCssFileName, launchEvent, livesync, lowMemoryEvent, notify, on, - orientationChanged, orientationChangedEvent, removeCssClass, resumeEvent, setApplication, suspendEvent + displayedEvent, exitEvent, getCssFileName, launchEvent, livesync, lowMemoryEvent, notify, on, + orientationChanged, orientationChangedEvent, resumeEvent, setApplication, suspendEvent, + systemAppearanceChanged, systemAppearanceChangedEvent } from "./application-common"; // First reexport so that app module is initialized. @@ -25,18 +27,12 @@ import { } from "../css/system-classes"; import { ios as iosView, View } from "../ui/core/view"; import { Frame, NavigationEntry } from "../ui/frame"; -import { UserInterfaceStyle } from "../ui/enums/enums"; import { device } from "../platform/platform"; import { profile } from "../profiling"; import { ios } from "../utils/utils"; const IOS_PLATFORM = "ios"; -const UI_STYLE_CSS_CLASSES = [ - `${CLASS_PREFIX}${UserInterfaceStyle.light}`, - `${CLASS_PREFIX}${UserInterfaceStyle.dark}` -]; - const getVisibleViewController = ios.getVisibleViewController; // NOTE: UIResponder with implementation of window - related to https://github.com/NativeScript/ios-runtime/issues/430 @@ -97,6 +93,7 @@ class IOSApplication implements IOSApplicationDefinition { private _observers: Array; private _orientation: "portrait" | "landscape" | "unknown"; private _rootView: View; + private _systemAppearance: "light" | "dark"; constructor() { this._observers = new Array(); @@ -121,6 +118,15 @@ class IOSApplication implements IOSApplicationDefinition { return this._window.rootViewController; } + get systemAppearance(): "light" | "dark" { + if (!this._systemAppearance) { + const userInterfaceStyle = this.rootController.traitCollection.userInterfaceStyle; + this._systemAppearance = getSystemAppearanceValue(userInterfaceStyle); + } + + return this._systemAppearance; + } + get nativeApp(): UIApplication { return UIApplication.sharedApplication; } @@ -293,9 +299,21 @@ class IOSApplication implements IOSApplicationDefinition { this._window.makeKeyAndVisible(); } - setupRootViewCssClasses(controller, rootView); + setupRootViewCssClasses(rootView); rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { - traitCollectionColorAppearanceChanged(controller, rootView); + const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; + const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); + + if (this._systemAppearance !== newSystemAppearance) { + this._systemAppearance = newSystemAppearance; + + notify({ + eventName: systemAppearanceChangedEvent, + ios: this, + newValue: this._systemAppearance, + object: this + }); + } }); } } @@ -312,17 +330,6 @@ setApplication(iosApp); let mainEntry: NavigationEntry; -function traitCollectionColorAppearanceChanged(controller: UIViewController, rootView: View) { - const newUserInterfaceStyle = controller.traitCollection.userInterfaceStyle; - const newUserInterfaceStyleValue = getUserInterfaceStyleValue(newUserInterfaceStyle); - const newUserInterfaceStyleCssClass = `${CLASS_PREFIX}${newUserInterfaceStyleValue}`; - - if (!rootView.cssClasses.has(newUserInterfaceStyleCssClass)) { - UI_STYLE_CSS_CLASSES.forEach(cssClass => removeCssClass(rootView, cssClass)); - applyCssClass(rootView, newUserInterfaceStyleCssClass); - } -} - function createRootView(v?: View) { let rootView = v; if (!rootView) { @@ -379,9 +386,21 @@ export function _start(entry?: string | NavigationEntry) { visibleVC.presentViewControllerAnimatedCompletion(controller, true, null); } - setupRootViewCssClasses(controller, rootView); + setupRootViewCssClasses(rootView); rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { - traitCollectionColorAppearanceChanged(controller, rootView); + const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; + const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); + + if (this._systemAppearance !== newSystemAppearance) { + this._systemAppearance = newSystemAppearance; + + notify({ + eventName: systemAppearanceChangedEvent, + ios: this, + newValue: this._systemAppearance, + object: this + }); + } }); iosApp.notifyAppStarted(); } @@ -413,7 +432,7 @@ export function getNativeApplication(): UIApplication { return iosApp.nativeApp; } -function getUserInterfaceStyleValue(userInterfaceStyle: number): "dark" | "light" | "unspecified" { +function getSystemAppearanceValue(userInterfaceStyle: number): "dark" | "light" { switch (userInterfaceStyle) { case UIUserInterfaceStyle.Unspecified: case UIUserInterfaceStyle.Light: @@ -449,17 +468,14 @@ function setViewControllerView(view: View): void { } } -function setupRootViewCssClasses(controller: UIViewController, rootView: View): void { +function setupRootViewCssClasses(rootView: View): void { resetRootViewCssClasses(); const deviceType = device.deviceType.toLowerCase(); - const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; - const userInterfaceStyleValue = getUserInterfaceStyleValue(userInterfaceStyle); - pushToRootViewCssClasses(`${CLASS_PREFIX}${IOS_PLATFORM}`); pushToRootViewCssClasses(`${CLASS_PREFIX}${deviceType}`); pushToRootViewCssClasses(`${CLASS_PREFIX}${iosApp.orientation}`); - pushToRootViewCssClasses(`${CLASS_PREFIX}${userInterfaceStyleValue}`); + pushToRootViewCssClasses(`${CLASS_PREFIX}${iosApp.systemAppearance}`); const rootViewCssClasses = getRootViewCssClasses(); rootViewCssClasses.forEach(c => rootView.cssClasses.add(c)); @@ -476,6 +492,13 @@ on(orientationChangedEvent, (args: OrientationChangedEventData) => { } }); +on(systemAppearanceChangedEvent, (args: SystemAppearanceChangedEventData) => { + const rootView = getRootView(); + if (rootView) { + systemAppearanceChanged(rootView, args.newValue); + } +}); + global.__onLiveSync = function __onLiveSync(context?: ModuleContext) { if (!started) { return; diff --git a/tns-core-modules/ui/enums/enums.ts b/tns-core-modules/ui/enums/enums.ts index d73ffb979..97558a2e2 100644 --- a/tns-core-modules/ui/enums/enums.ts +++ b/tns-core-modules/ui/enums/enums.ts @@ -188,7 +188,7 @@ export module StatusBarStyle { export const dark = "dark"; } -export module UserInterfaceStyle { +export module SystemAppearance { export const light = "light"; export const dark = "dark"; } diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index fc52f37b8..b35a2e844 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -1289,6 +1289,7 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { pushToRootViewCssClasses(`${CLASS_PREFIX}${ANDROID_PLATFORM}`); pushToRootViewCssClasses(`${CLASS_PREFIX}${deviceType}`); pushToRootViewCssClasses(`${CLASS_PREFIX}${application.android.orientation}`); + pushToRootViewCssClasses(`${CLASS_PREFIX}${application.android.systemAppearance}`); const rootViewCssClasses = getRootViewCssClasses(); rootViewCssClasses.forEach(c => this._rootView.cssClasses.add(c));