From 0ffc790d82a39643ee4341f321a48e121fb572cb Mon Sep 17 00:00:00 2001 From: Martin Yankov Date: Thu, 28 Nov 2019 13:36:34 +0200 Subject: [PATCH] chore: remove critical circular dependencies (#8114) * chore: remove critical circular dependencies * chore: fix tslint errors * chore: remove platform specific types from interfaces * chore: update unit tests polyfills * fix: incorrect null check * chore: update api.md file * test: improve test case * chore: apply comments * test: avoid page style leaks in tests --- api-reports/NativeScript.api.md | 4 +- e2e/ui-tests-app/app/app.ts | 4 +- .../bottom-navigation-css-page.css | 1 + .../app/button/background-page.css | 1 + .../margins-paddings-with-percentage-page.css | 21 + .../margins-paddings-with-percentage-page.ts | 51 +-- .../margins-paddings-with-percentage-page.xml | 2 +- .../app/flexbox/flexbox-css-page.css | 1 + .../app/page/status-bar-css-page.css | 1 + .../app/tab-view/tab-view-css-page.css | 2 + .../app/tab-view/text-transform-page.css | 1 + .../application/application-common.ts | 47 +-- .../application/application-interfaces.ts | 79 ++++ .../application/application.android.ts | 34 +- .../application/application.d.ts | 11 +- .../application/application.ios.ts | 56 +-- .../data/observable/observable-interfaces.ts | 13 + .../data/observable/observable.ts | 9 +- .../debugger/devtools-elements-interfaces.ts | 18 + .../debugger/devtools-elements.android.ts | 7 +- .../debugger/devtools-elements.common.ts | 7 +- .../debugger/devtools-elements.ios.ts | 7 +- nativescript-core/debugger/dom-node.ts | 2 +- .../file-name-resolver/file-name-resolver.ts | 3 +- .../file-system/file-system-access.android.ts | 2 +- .../file-system/file-system-access.ios.ts | 2 +- .../ui/animation/animation-common.ts | 30 +- .../ui/animation/animation-interfaces.ts | 72 ++++ .../ui/animation/animation.android.ts | 16 +- .../ui/animation/animation.ios.ts | 27 +- .../ui/core/bindable/bindable-resources.ts | 9 + .../ui/core/bindable/bindable.ts | 7 +- .../ui/core/view-base/view-base.d.ts | 5 + .../ui/core/view-base/view-base.ts | 6 + nativescript-core/ui/core/view/view-common.ts | 183 +------- .../ui/core/view/view-helper/package.json | 5 + .../view/view-helper/view-helper-common.ts | 207 +++++++++ .../view/view-helper/view-helper.android.ts | 1 + .../ui/core/view/view-helper/view-helper.d.ts | 63 +++ .../core/view/view-helper/view-helper.ios.ts | 365 ++++++++++++++++ nativescript-core/ui/core/view/view.ios.ts | 395 ++---------------- .../ui/frame/fragment.transitions.ios.ts | 2 +- nativescript-core/ui/frame/frame-common.ts | 29 +- .../ui/frame/frame-interfaces.ts | 107 +++++ nativescript-core/ui/frame/frame.android.ts | 58 +-- nativescript-core/ui/frame/frame.ios.ts | 33 +- nativescript-core/ui/styling/font-common.ts | 5 +- .../ui/styling/font-interfaces.ts | 12 + nativescript-core/ui/styling/font.d.ts | 2 +- nativescript-core/ui/styling/style-scope.ts | 26 +- .../tab-strip-item/tab-strip-item.ts | 2 +- .../ui/text-base/text-base-common.ts | 18 +- .../ui/text-base/text-base-interfaces.ts | 11 + .../ui/text-base/text-base.android.ts | 9 +- .../ui/text-base/text-base.ios.ts | 10 +- .../ui/web-view/web-view-common.ts | 10 +- .../ui/web-view/web-view-interfaces.ts | 14 + .../ui/web-view/web-view.android.ts | 6 +- nativescript-core/ui/web-view/web-view.ios.ts | 3 +- .../layout-helper/layout-helper-common.ts | 63 +++ .../layout-helper/layout-helper.android.ts | 49 +++ .../utils/layout-helper/layout-helper.d.ts | 77 ++++ .../utils/layout-helper/layout-helper.ios.ts | 36 ++ .../utils/layout-helper/package.json | 6 + .../utils/native-helper.android.ts | 156 +++++++ nativescript-core/utils/native-helper.d.ts | 159 +++++++ nativescript-core/utils/native-helper.ios.ts | 138 ++++++ nativescript-core/utils/utils-common.ts | 66 +-- nativescript-core/utils/utils.android.ts | 215 +--------- nativescript-core/utils/utils.ios.ts | 163 +------- tests/webpack.config.js | 2 +- unit-tests/runtime.ts | 1 + 72 files changed, 1958 insertions(+), 1307 deletions(-) create mode 100644 e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css create mode 100644 e2e/ui-tests-app/app/button/background-page.css create mode 100644 e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css create mode 100644 e2e/ui-tests-app/app/flexbox/flexbox-css-page.css create mode 100644 e2e/ui-tests-app/app/page/status-bar-css-page.css create mode 100644 e2e/ui-tests-app/app/tab-view/tab-view-css-page.css create mode 100644 e2e/ui-tests-app/app/tab-view/text-transform-page.css create mode 100644 nativescript-core/application/application-interfaces.ts create mode 100644 nativescript-core/data/observable/observable-interfaces.ts create mode 100644 nativescript-core/debugger/devtools-elements-interfaces.ts create mode 100644 nativescript-core/ui/animation/animation-interfaces.ts create mode 100644 nativescript-core/ui/core/bindable/bindable-resources.ts create mode 100644 nativescript-core/ui/core/view/view-helper/package.json create mode 100644 nativescript-core/ui/core/view/view-helper/view-helper-common.ts create mode 100644 nativescript-core/ui/core/view/view-helper/view-helper.android.ts create mode 100644 nativescript-core/ui/core/view/view-helper/view-helper.d.ts create mode 100644 nativescript-core/ui/core/view/view-helper/view-helper.ios.ts create mode 100644 nativescript-core/ui/frame/frame-interfaces.ts create mode 100644 nativescript-core/ui/styling/font-interfaces.ts create mode 100644 nativescript-core/ui/text-base/text-base-interfaces.ts create mode 100644 nativescript-core/ui/web-view/web-view-interfaces.ts create mode 100644 nativescript-core/utils/layout-helper/layout-helper-common.ts create mode 100644 nativescript-core/utils/layout-helper/layout-helper.android.ts create mode 100644 nativescript-core/utils/layout-helper/layout-helper.d.ts create mode 100644 nativescript-core/utils/layout-helper/layout-helper.ios.ts create mode 100644 nativescript-core/utils/layout-helper/package.json create mode 100644 nativescript-core/utils/native-helper.android.ts create mode 100644 nativescript-core/utils/native-helper.d.ts create mode 100644 nativescript-core/utils/native-helper.ios.ts diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index 09cb099af..c047e2f30 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -1206,7 +1206,7 @@ export class ImageSource { export type InstrumentationMode = "counters" | "timeline" | "lifecycle"; // @public -export interface iOSApplication { +export class iOSApplication { /* tslint:enable */ addNotificationObserver(notificationName: string, onReceiveCallback: (notification: any /* NSNotification */) => void): any; @@ -2861,6 +2861,8 @@ export abstract class ViewBase extends Observable { // (undocumented) _isStyleScopeHost: boolean; // (undocumented) + public _layoutParent(): void; + // (undocumented) left: Length; public static loadedEvent: string; public loadView(view: ViewBase): void; diff --git a/e2e/ui-tests-app/app/app.ts b/e2e/ui-tests-app/app/app.ts index b2819d214..169e9b56a 100644 --- a/e2e/ui-tests-app/app/app.ts +++ b/e2e/ui-tests-app/app/app.ts @@ -88,9 +88,7 @@ application.on(application.discardedErrorEvent, function (args: application.Disc console.log("### [Discarded] stack: " + args.error.stack); }); -application.setCssFileName("app.css"); -application._start({ moduleName: "main-page" }); +application.run({ moduleName: "app-root" }); // TODO: investigate tab-view -> tabviewcss test crash // TODO: investigate css -> layouts border overlap failure -// application.run({ moduleName: "app-root" }); diff --git a/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css b/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/bottom-navigation/bottom-navigation-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/button/background-page.css b/e2e/ui-tests-app/app/button/background-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/button/background-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css new file mode 100644 index 000000000..d67a49649 --- /dev/null +++ b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.css @@ -0,0 +1,21 @@ +.with-percent { background-color: orange; font-size: 8; } +.with-percent GridLayout { margin: 3%; background-color: lightgreen; font-size: 8; } +.with-percent StackLayout { border-color: red; border-width: 1; } +.with-percent StackLayout * { border-color: blue; border-width: 1; } +.with-percent GridLayout { border-color: green; border-width: 1; } +.with-percent .test1 { padding: 10%; } +.with-percent .test2 { padding: 10%; background-color: lightblue; } +.with-percent .test3 { margin: 1% 2% 3% 4%; } +.with-percent WrapLayout { orientation: vertical; width: 75%; height: 45% } +.with-percent Button { color: black } + +.without-percent { background-color: orange; font-size: 8; } +.without-percent GridLayout { margin:3; background-color: lightgreen; font-size: 8; } +.without-percent StackLayout { border-color: red; border-width: 1; } +.without-percent StackLayout * { border-color: blue; border-width: 1; } +.without-percent GridLayout { border-color: green; border-width: 1; } +.without-percent .test1 { padding: 10; } +.without-percent .test2 { padding: 10; background-color: lightblue; } +.without-percent .test3 { margin: 10 20 30 40; } +.without-percent WrapLayout { orientation: vertical; width: 100; height: 120; } +.without-percent Button { color: black } \ No newline at end of file diff --git a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts index 016ea3269..9ef74f237 100644 --- a/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts +++ b/e2e/ui-tests-app/app/css/margins-paddings-with-percentage-page.ts @@ -1,53 +1,26 @@ -import * as view from "tns-core-modules/ui/core/view"; -import * as pages from "tns-core-modules/ui/page"; +import { View, getViewById } from "tns-core-modules/ui/core/view"; import { EventData } from "tns-core-modules/data/observable"; -import * as button from "tns-core-modules/ui/button"; +import { Button } from "tns-core-modules/ui/button"; -const cssPercentage = ` - Page { background-color: orange; font-size: 8; } - GridLayout { margin: 3%; background-color: lightgreen; font-size: 8; } - StackLayout { border-color: red; border-width: 1; } - StackLayout * { border-color: blue; border-width: 1; } - GridLayout { border-color: green; border-width: 1; } - .test1 { padding: 10%; } - .test2 { padding: 10%; background-color: lightblue; } - .test3 { margin: 1% 2% 3% 4%; } - WrapLayout { orientation: vertical; width: 75%; height: 45% } - Button { color: black }`; - -const cssWithouPercentage = ` - Page { background-color: orange; font-size: 8; } - GridLayout { margin:3; background-color: lightgreen; font-size: 8; } - StackLayout { border-color: red; border-width: 1; } - StackLayout * { border-color: blue; border-width: 1; } - GridLayout { border-color: green; border-width: 1; } - .test1 { padding: 10; } - .test2 { padding: 10; background-color: lightblue; } - .test3 { margin: 10 20 30 40; } - WrapLayout { orientation: vertical; width: 100; height: 120; } - Button { color: black }`; - -var isSCCWithPercentage = true; +let isSCCWithPercentage = true; export function pageLoaded(args: EventData) { - let page = args.object; - page.css = cssPercentage; - getBtnText(args); + setBtnText(args); } export function applyTap(args: EventData) { - let page = (args.object).page; - let css = isSCCWithPercentage ? cssWithouPercentage : cssPercentage; + let page = (args.object).page; + let cssClass = isSCCWithPercentage ? "without-percent" : "with-percent"; isSCCWithPercentage = !isSCCWithPercentage; - console.log(css); - page.css = css; - getBtnText(args); + console.log(cssClass); + page.className = cssClass; + setBtnText(args); } -function getBtnText(args: EventData) { - var parent = (args.object).parent; +function setBtnText(args: EventData) { + let parent = (args.object).parent; if (parent) { - var btn = view.getViewById(parent, "button"); + var btn = diff --git a/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css b/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/flexbox/flexbox-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/page/status-bar-css-page.css b/e2e/ui-tests-app/app/page/status-bar-css-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/page/status-bar-css-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css b/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css new file mode 100644 index 000000000..6e1469711 --- /dev/null +++ b/e2e/ui-tests-app/app/tab-view/tab-view-css-page.css @@ -0,0 +1,2 @@ +/* Currently need an empty css file to make this module a style scope host. +This tests sets css on the module and without this empty file the css is set on the frame on top of it */ \ No newline at end of file diff --git a/e2e/ui-tests-app/app/tab-view/text-transform-page.css b/e2e/ui-tests-app/app/tab-view/text-transform-page.css new file mode 100644 index 000000000..d688ad6e7 --- /dev/null +++ b/e2e/ui-tests-app/app/tab-view/text-transform-page.css @@ -0,0 +1 @@ +/* Empty CSS to avoid styles leaking from the page - https://github.com/NativeScript/NativeScript/issues/8143 */ diff --git a/nativescript-core/application/application-common.ts b/nativescript-core/application/application-common.ts index 10de66855..4d376f34d 100644 --- a/nativescript-core/application/application-common.ts +++ b/nativescript-core/application/application-common.ts @@ -1,13 +1,29 @@ // Require globals first so that snapshot takes __extends function. import "../globals"; -import { Observable, EventData } from "../data/observable"; + +// Types +import { AndroidApplication, iOSApplication } from "."; +import { + CssChangedEventData, DiscardedErrorEventData, + LoadAppCSSEventData, UnhandledErrorEventData +} from "./application-interfaces"; +import { EventData } from "../data/observable/observable-interfaces"; import { View } from "../ui/core/view"; + +// Requires +import { Observable } from "../data/observable"; import { trace as profilingTrace, time, uptime, level as profilingLevel, } from "../profiling"; +import * as bindableResources from "../ui/core/bindable/bindable-resources"; +import { CLASS_PREFIX, pushToRootViewCssClasses, removeFromRootViewCssClasses } from "../css/system-classes"; +import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums"; + +export { Observable }; +export * from "./application-interfaces"; const events = new Observable(); let launched = false; @@ -30,22 +46,6 @@ export function hasLaunched(): boolean { return launched; } -export { Observable }; - -import { - AndroidApplication, - CssChangedEventData, - DiscardedErrorEventData, - iOSApplication, - LoadAppCSSEventData, - UnhandledErrorEventData -} from "./application"; - -import { CLASS_PREFIX, pushToRootViewCssClasses, removeFromRootViewCssClasses } from "../css/system-classes"; -import { DeviceOrientation, SystemAppearance } from "../ui/enums/enums"; - -export { UnhandledErrorEventData, DiscardedErrorEventData, CssChangedEventData, LoadAppCSSEventData }; - export const launchEvent = "launch"; export const suspendEvent = "suspend"; export const displayedEvent = "displayed"; @@ -70,18 +70,16 @@ const SYSTEM_APPEARANCE_CSS_CLASSES = [ let cssFile: string = "./app.css"; -let resources: any = {}; - export function getResources() { - return resources; + return bindableResources.get(); } export function setResources(res: any) { - resources = res; + bindableResources.set(res); } -export let android = undefined; -export let ios = undefined; +export let android: AndroidApplication = undefined; +export let ios: iOSApplication = undefined; export const on: typeof events.on = events.on.bind(events); export const off: typeof events.off = events.off.bind(events); @@ -127,8 +125,7 @@ export function loadAppCss(): void { try { events.notify({ eventName: "loadAppCss", object: app, cssFile: getCssFileName() }); } catch (e) { - throw new Error(`The file ${getCssFileName()} couldn't be loaded! ` + - `You may need to register it inside ./app/vendor.ts.`); + throw new Error(`The app CSS file ${getCssFileName()} couldn't be loaded!`); } } diff --git a/nativescript-core/application/application-interfaces.ts b/nativescript-core/application/application-interfaces.ts new file mode 100644 index 000000000..eb35c77ba --- /dev/null +++ b/nativescript-core/application/application-interfaces.ts @@ -0,0 +1,79 @@ +// Types +import { EventData } from "../data/observable/observable-interfaces"; +import { View } from "../ui/core/view"; + +export interface ApplicationEventData extends EventData { + ios?: any; + android?: any; + eventName: string; + object: any; +} + +export interface LaunchEventData extends ApplicationEventData { + root?: View; + savedInstanceState?: any /* android.os.Bundle */; +} + +export interface OrientationChangedEventData extends ApplicationEventData { + newValue: "portrait" | "landscape" | "unknown"; +} + +export interface SystemAppearanceChangedEventData extends ApplicationEventData { + newValue: "light" | "dark"; +} + +export interface UnhandledErrorEventData extends ApplicationEventData { + ios?: NativeScriptError; + android?: NativeScriptError; + error: NativeScriptError; +} + +export interface DiscardedErrorEventData extends ApplicationEventData { + error: NativeScriptError; +} + +export interface CssChangedEventData extends EventData { + cssFile?: string; + cssText?: string; +} + +export interface AndroidActivityEventData { + activity: any /* androidx.appcompat.app.AppCompatActivity */; + eventName: string; + object: any; +} + +export interface AndroidActivityBundleEventData extends AndroidActivityEventData { + bundle: any /* android.os.Bundle */; +} + +export interface AndroidActivityRequestPermissionsEventData extends AndroidActivityEventData { + requestCode: number; + permissions: Array; + grantResults: Array; +} + +export interface AndroidActivityResultEventData extends AndroidActivityEventData { + requestCode: number; + resultCode: number; + intent: any /* android.content.Intent */; +} + +export interface AndroidActivityNewIntentEventData extends AndroidActivityEventData { + intent: any /* android.content.Intent */; +} + +export interface AndroidActivityBackPressedEventData extends AndroidActivityEventData { + cancel: boolean; +} + +/** +* @deprecated +*/ +export interface RootViewControllerImpl { + contentController: any; +} + +export interface LoadAppCSSEventData extends EventData { + cssFile: string; +} \ No newline at end of file diff --git a/nativescript-core/application/application.android.ts b/nativescript-core/application/application.android.ts index b14ac1632..bf45c01c1 100644 --- a/nativescript-core/application/application.android.ts +++ b/nativescript-core/application/application.android.ts @@ -1,4 +1,5 @@ -// Definitions. +// Types. +import { AndroidApplication as AndroidApplicationDefinition } from "."; import { AndroidActivityBackPressedEventData, AndroidActivityBundleEventData, @@ -6,26 +7,24 @@ import { AndroidActivityNewIntentEventData, AndroidActivityRequestPermissionsEventData, AndroidActivityResultEventData, - AndroidApplication as AndroidApplicationDefinition, ApplicationEventData, CssChangedEventData, OrientationChangedEventData, SystemAppearanceChangedEventData -} from "."; +} from "./application-interfaces"; +import { View } from "../ui/core/view"; +import { NavigationEntry, AndroidActivityCallbacks } from "../ui/frame/frame-interfaces"; +// Requires import { - displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, on, + displayedEvent, hasListeners, livesync, lowMemoryEvent, notify, Observable, orientationChanged, orientationChangedEvent, setApplication, suspendEvent, systemAppearanceChanged, systemAppearanceChangedEvent } from "./application-common"; - -import { profile } from "../profiling"; - // First reexport so that app module is initialized. export * from "./application-common"; -// Types. -import { NavigationEntry, View, AndroidActivityCallbacks } from "../ui/frame"; +import { profile } from "../profiling"; const ActivityCreated = "activityCreated"; const ActivityDestroyed = "activityDestroyed"; @@ -149,6 +148,10 @@ export class AndroidApplication extends Observable implements AndroidApplication } } +// HACK: Use an interface with the same name, so that the class above fulfills the 'implements' requirement +// HACK: We use the 'implements' to verify the class above is the same as the one declared in the d.ts +// HACK: We declare all these 'on' statements, so that they can appear in the API reference +// HACK: Do we need this? Is it useful? There are static fields to the AndroidApplication class for the event names. export interface AndroidApplication { on(eventNames: string, callback: (data: AndroidActivityEventData) => void, thisArg?: any); on(event: "activityCreated", callback: (args: AndroidActivityBundleEventData) => void, thisArg?: any); @@ -171,9 +174,8 @@ setApplication(androidApp); let mainEntry: NavigationEntry; let started = false; -const createRootFrame = { value: true }; -export function _start(entry?: NavigationEntry | string) { +export function run(entry?: NavigationEntry | string) { if (started) { throw new Error("Application is already started."); } @@ -186,15 +188,6 @@ export function _start(entry?: NavigationEntry | string) { } } -export function _shouldCreateRootFrame(): boolean { - return createRootFrame.value; -} - -export function run(entry?: NavigationEntry | string) { - createRootFrame.value = false; - _start(entry); -} - export function addCss(cssText: string, attributeScoped?: boolean): void { notify({ eventName: "cssChanged", object: androidApp, cssText: cssText }); if (!attributeScoped) { @@ -213,7 +206,6 @@ export function _resetRootView(entry?: NavigationEntry | string) { throw new Error("Cannot find android activity."); } - createRootFrame.value = false; mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; const callbacks: AndroidActivityCallbacks = activity[CALLBACKS]; if (!callbacks) { diff --git a/nativescript-core/application/application.d.ts b/nativescript-core/application/application.d.ts index ddab72616..6649b8d92 100644 --- a/nativescript-core/application/application.d.ts +++ b/nativescript-core/application/application.d.ts @@ -211,15 +211,6 @@ export function run(entry?: NavigationEntry | string); */ export function _resetRootView(entry?: NavigationEntry | string); -/** - * @private - */ -export function _shouldCreateRootFrame(): boolean; -/** - * @private - */ -export function _start(entry?: NavigationEntry | string); - /** * A basic method signature to hook an event listener (shortcut alias to the addEventListener method). * @param eventNames - String corresponding to events (e.g. "onLaunch"). Optionally could be used more events separated by `,` (e.g. "onLaunch", "onSuspend"). @@ -610,7 +601,7 @@ export class AndroidApplication extends Observable { /** * The abstraction of an iOS-specific application object. */ -export interface iOSApplication { +export class iOSApplication { /* tslint:enable */ /** * The root view controller for the application. diff --git a/nativescript-core/application/application.ios.ts b/nativescript-core/application/application.ios.ts index 13efff29b..33374a556 100644 --- a/nativescript-core/application/application.ios.ts +++ b/nativescript-core/application/application.ios.ts @@ -1,19 +1,22 @@ +// Types +import { iOSApplication as iOSApplicationDefinition } from "."; import { ApplicationEventData, CssChangedEventData, - iOSApplication as IOSApplicationDefinition, LaunchEventData, LoadAppCSSEventData, OrientationChangedEventData, SystemAppearanceChangedEventData -} from "."; +} from "./application-interfaces"; +import { View } from "../ui/core/view"; +import { NavigationEntry } from "../ui/frame/frame-interfaces"; +// Require import { 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. export * from "./application-common"; @@ -24,9 +27,7 @@ import { getRootViewCssClasses, pushToRootViewCssClasses } from "../css/system-classes"; - -import { ios as iosView, View } from "../ui/core/view"; -import { Frame, NavigationEntry } from "../ui/frame"; +import { ios as iosViewHelper } from "../ui/core/view/view-helper"; import { device } from "../platform/platform"; import { profile } from "../profiling"; import { ios } from "../utils/utils"; @@ -88,7 +89,9 @@ class CADisplayLinkTarget extends NSObject { }; } -class IOSApplication implements IOSApplicationDefinition { +/* tslint:disable */ +export class iOSApplication implements iOSApplicationDefinition { + /* tslint:enable */ private _backgroundColor = majorVersion <= 12 ? UIColor.whiteColor : UIColor.systemBackgroundColor; private _delegate: typeof UIApplicationDelegate; private _window: UIWindow; @@ -293,13 +296,8 @@ class IOSApplication implements IOSApplicationDefinition { this._rootView = rootView; - if (createRootFrame.value) { - // Don't setup as styleScopeHost - rootView._setupUI({}); - } else { - // setup view as styleScopeHost - rootView._setupAsRootView({}); - } + // setup view as styleScopeHost + rootView._setupAsRootView({}); setViewControllerView(rootView); @@ -312,7 +310,7 @@ class IOSApplication implements IOSApplicationDefinition { this._window.makeKeyAndVisible(); } - rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { + rootView.on(iosViewHelper.traitCollectionColorAppearanceChangedEvent, () => { const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); @@ -331,8 +329,9 @@ class IOSApplication implements IOSApplicationDefinition { } } -const iosApp = new IOSApplication(); - +/* tslint:disable */ +const iosApp = new iOSApplication(); +/* tslint:enable */ export { iosApp as ios }; setApplication(iosApp); @@ -350,12 +349,7 @@ function createRootView(v?: View) { if (!mainEntry) { throw new Error("Main entry is missing. App cannot be started. Verify app bootstrap."); } else { - if (createRootFrame.value) { - const frame = rootView = new Frame(); - frame.navigate(mainEntry); - } else { - rootView = Builder.createViewFromEntry(mainEntry); - } + rootView = Builder.createViewFromEntry(mainEntry); } } @@ -372,10 +366,8 @@ export function getRootView() { return iosApp.rootView; } -// NOTE: for backwards compatibility. Remove for 4.0.0. -const createRootFrame = { value: true }; let started: boolean = false; -export function _start(entry?: string | NavigationEntry) { +export function run(entry?: string | NavigationEntry) { mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; started = true; @@ -404,7 +396,7 @@ export function _start(entry?: string | NavigationEntry) { // Mind root view CSS classes in future work // on embedding NativeScript applications setRootViewSystemAppearanceCssClass(rootView); - rootView.on(iosView.traitCollectionColorAppearanceChangedEvent, () => { + rootView.on(iosViewHelper.traitCollectionColorAppearanceChangedEvent, () => { const userInterfaceStyle = controller.traitCollection.userInterfaceStyle; const newSystemAppearance = getSystemAppearanceValue(userInterfaceStyle); @@ -426,11 +418,6 @@ export function _start(entry?: string | NavigationEntry) { } } -export function run(entry?: string | NavigationEntry) { - createRootFrame.value = false; - _start(entry); -} - export function addCss(cssText: string, attributeScoped?: boolean): void { notify({ eventName: "cssChanged", object: iosApp, cssText: cssText }); if (!attributeScoped) { @@ -442,7 +429,6 @@ export function addCss(cssText: string, attributeScoped?: boolean): void { } export function _resetRootView(entry?: NavigationEntry | string) { - createRootFrame.value = false; mainEntry = typeof entry === "string" ? { moduleName: entry } : entry; iosApp.setWindowContent(); } @@ -467,7 +453,7 @@ function getViewController(rootView: View): UIViewController { if (!(viewController instanceof UIViewController)) { // We set UILayoutViewController dynamically to the root view if it doesn't have a view controller // At the moment the root view doesn't have its native view created. We set it in the setViewControllerView func - viewController = iosView.UILayoutViewController.initWithOwner(new WeakRef(rootView)) as UIViewController; + viewController = iosViewHelper.UILayoutViewController.initWithOwner(new WeakRef(rootView)) as UIViewController; rootView.viewController = viewController; } @@ -482,7 +468,7 @@ function setViewControllerView(view: View): void { throw new Error("Root should be either UIViewController or UIView"); } - if (viewController instanceof iosView.UILayoutViewController) { + if (viewController instanceof iosViewHelper.UILayoutViewController) { viewController.view.addSubview(nativeView); } } diff --git a/nativescript-core/data/observable/observable-interfaces.ts b/nativescript-core/data/observable/observable-interfaces.ts new file mode 100644 index 000000000..4e2ac5559 --- /dev/null +++ b/nativescript-core/data/observable/observable-interfaces.ts @@ -0,0 +1,13 @@ +// Types +import { Observable } from "."; + +export interface EventData { + eventName: string; + object: Observable; +} + +export interface PropertyChangeData extends EventData { + propertyName: string; + value: any; + oldValue?: any; +} \ No newline at end of file diff --git a/nativescript-core/data/observable/observable.ts b/nativescript-core/data/observable/observable.ts index adb8ab39c..18f4699b9 100644 --- a/nativescript-core/data/observable/observable.ts +++ b/nativescript-core/data/observable/observable.ts @@ -1,10 +1,7 @@ -import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinition, PropertyChangeData } from "."; +import { Observable as ObservableDefinition, WrappedValue as WrappedValueDefinition } from "."; +import { EventData, PropertyChangeData } from "./observable-interfaces"; -// TODO: Remove this. It is the same export as in d.ts to fix failing build when modules are linked -export interface EventData { - eventName: string; - object: ObservableDefinition; -} +export * from "./observable-interfaces"; interface ListenerEntry { callback: (data: EventData) => void; diff --git a/nativescript-core/debugger/devtools-elements-interfaces.ts b/nativescript-core/debugger/devtools-elements-interfaces.ts new file mode 100644 index 000000000..c7bf5eee2 --- /dev/null +++ b/nativescript-core/debugger/devtools-elements-interfaces.ts @@ -0,0 +1,18 @@ +//Types +import { DOMNode } from "./dom-node"; + +export interface InspectorCommands { + // DevTools -> Application communication. Methods that devtools calls when needed. + getDocument(): string | DOMNode; + removeNode(nodeId: number): void; + getComputedStylesForNode(nodeId: number): string | Array<{ name: string, value: string }>; + setAttributeAsText(nodeId: number, text: string, name: string): void; +} + +export interface InspectorEvents { + // Application -> DevTools communication. Methods that the app should call when needed. + childNodeInserted(parentId: number, lastId: number, node: DOMNode): void; + childNodeRemoved(parentId: number, nodeId: number): void; + attributeModified(nodeId: number, attrName: string, attrValue: string): void; + attributeRemoved(nodeId: number, attrName: string): void; +} \ No newline at end of file diff --git a/nativescript-core/debugger/devtools-elements.android.ts b/nativescript-core/debugger/devtools-elements.android.ts index be0753b3f..96889f6ad 100644 --- a/nativescript-core/debugger/devtools-elements.android.ts +++ b/nativescript-core/debugger/devtools-elements.android.ts @@ -1,7 +1,12 @@ -import { InspectorEvents, InspectorCommands } from "./devtools-elements"; +// Types +import { InspectorEvents, InspectorCommands } from "./devtools-elements-interfaces"; + +// Requires import { getDocument, getComputedStylesForNode, removeNode, setAttributeAsText } from "./devtools-elements.common"; import { registerInspectorEvents, DOMNode } from "./dom-node"; +export * from "./devtools-elements-interfaces"; + export function attachDOMInspectorEventCallbacks(DOMDomainFrontend: InspectorEvents) { registerInspectorEvents(DOMDomainFrontend); diff --git a/nativescript-core/debugger/devtools-elements.common.ts b/nativescript-core/debugger/devtools-elements.common.ts index 71473ef5a..3fa3bbf40 100644 --- a/nativescript-core/debugger/devtools-elements.common.ts +++ b/nativescript-core/debugger/devtools-elements.common.ts @@ -1,7 +1,8 @@ -import { getNodeById } from "./dom-node"; - -// Needed for typings only +// Types import { ViewBase } from "../ui/core/view-base"; + +//Requires +import { getNodeById } from "./dom-node"; import { mainThreadify } from "../utils/utils"; // Use lazy requires for core modules diff --git a/nativescript-core/debugger/devtools-elements.ios.ts b/nativescript-core/debugger/devtools-elements.ios.ts index 2d7046639..43e9dec19 100644 --- a/nativescript-core/debugger/devtools-elements.ios.ts +++ b/nativescript-core/debugger/devtools-elements.ios.ts @@ -1,7 +1,12 @@ -import { InspectorEvents, InspectorCommands } from "./devtools-elements"; +// Types +import { InspectorEvents, InspectorCommands } from "./devtools-elements-interfaces"; + +// Requires import { getDocument, getComputedStylesForNode, removeNode, setAttributeAsText } from "./devtools-elements.common"; import { registerInspectorEvents, DOMNode } from "./dom-node"; +export * from "./devtools-elements-interfaces"; + export function attachDOMInspectorEventCallbacks(DOMDomainFrontend: InspectorEvents) { registerInspectorEvents(DOMDomainFrontend); diff --git a/nativescript-core/debugger/dom-node.ts b/nativescript-core/debugger/dom-node.ts index f8ca58288..9966ac0ed 100644 --- a/nativescript-core/debugger/dom-node.ts +++ b/nativescript-core/debugger/dom-node.ts @@ -1,5 +1,5 @@ import { CSSComputedStyleProperty } from "./css-agent"; -import { InspectorEvents } from "./devtools-elements"; +import { InspectorEvents } from "./devtools-elements-interfaces"; // Needed for typings only import { ViewBase } from "../ui/core/view"; diff --git a/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts b/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts index 608e275d0..6dae02d0c 100644 --- a/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts +++ b/nativescript-core/file-system/file-name-resolver/file-name-resolver.ts @@ -1,4 +1,4 @@ -import { PlatformContext, FileNameResolver as FileNameResolverDefinition } from "../file-name-resolver"; +import { PlatformContext, FileNameResolver as FileNameResolverDefinition } from "."; import { screen, device } from "../../platform"; import { path as fsPath, Folder, File } from "../file-system"; import * as trace from "../../trace"; @@ -6,7 +6,6 @@ import * as appCommonModule from "../../application/application-common"; import { findMatch } from "../../module-name-resolver/qualifier-matcher/qualifier-matcher"; -@Deprecated export class FileNameResolver implements FileNameResolverDefinition { private _context: PlatformContext; private _cache = {}; diff --git a/nativescript-core/file-system/file-system-access.android.ts b/nativescript-core/file-system/file-system-access.android.ts index 8c2a7077f..9d73e96ec 100644 --- a/nativescript-core/file-system/file-system-access.android.ts +++ b/nativescript-core/file-system/file-system-access.android.ts @@ -495,7 +495,7 @@ export class FileSystemAccess { // TODO: This method is the same as in the iOS implementation. // Make it in a separate file / module so it can be reused from both implementations. - private getFileExtension(path: string): string { + public getFileExtension(path: string): string { const dotIndex = path.lastIndexOf("."); if (dotIndex && dotIndex >= 0 && dotIndex < path.length) { return path.substring(dotIndex); diff --git a/nativescript-core/file-system/file-system-access.ios.ts b/nativescript-core/file-system/file-system-access.ios.ts index d63f4b4a8..7bca2d71d 100644 --- a/nativescript-core/file-system/file-system-access.ios.ts +++ b/nativescript-core/file-system/file-system-access.ios.ts @@ -395,7 +395,7 @@ export class FileSystemAccess { // TODO: This method is the same as in the iOS implementation. // Make it in a separate file / module so it can be reused from both implementations. - private getFileExtension(path: string): string { + public getFileExtension(path: string): string { // TODO [For Panata]: The definitions currently specify "any" as a return value of this method //const nsString = Foundation.NSString.stringWithString(path); //const extension = nsString.pathExtension(); diff --git a/nativescript-core/ui/animation/animation-common.ts b/nativescript-core/ui/animation/animation-common.ts index 1c6e5bf42..9a7ace2ef 100644 --- a/nativescript-core/ui/animation/animation-common.ts +++ b/nativescript-core/ui/animation/animation-common.ts @@ -1,19 +1,23 @@ -// Definitions. +// Types. import { CubicBezierAnimationCurve as CubicBezierAnimationCurveDefinition, - AnimationPromise as AnimationPromiseDefinition, - Animation as AnimationBaseDefinition, - AnimationDefinition, - Pair + Animation as AnimationBaseDefinition } from "."; -import { View } from "../core/view"; +import { + AnimationDefinition, AnimationPromise as AnimationPromiseDefinition, + Pair, PropertyAnimation +} from "./animation-interfaces"; -// Types. +// Requires. import { Color } from "../../color"; -import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceType } from "../../trace"; +import { + isEnabled as traceEnabled, write as traceWrite, + categories as traceCategories, messageType as traceType +} from "../../trace"; import { PercentLength } from "../styling/style-properties"; export { Color, traceEnabled, traceWrite, traceCategories, traceType }; +export * from "./animation-interfaces"; export module Properties { export const opacity = "opacity"; @@ -25,16 +29,6 @@ export module Properties { export const width = "width"; } -export interface PropertyAnimation { - target: View; - property: string; - value: any; - duration?: number; - delay?: number; - iterations?: number; - curve?: any; -} - export class CubicBezierAnimationCurve implements CubicBezierAnimationCurveDefinition { public x1: number; diff --git a/nativescript-core/ui/animation/animation-interfaces.ts b/nativescript-core/ui/animation/animation-interfaces.ts new file mode 100644 index 000000000..d086c7e42 --- /dev/null +++ b/nativescript-core/ui/animation/animation-interfaces.ts @@ -0,0 +1,72 @@ +// Types +import { View } from "../core/view"; +import { PercentLength } from "../styling/style-properties"; +import { Color } from "../../color"; + +export type Transformation = { + property: TransformationType; + value: TransformationValue; +}; + +export type TransformationType = "rotate" | + "translate" | "translateX" | "translateY" | + "scale" | "scaleX" | "scaleY"; + +export type TransformationValue = Pair | number; + +export type TransformFunctionsInfo = { + translate: Pair, + rotate: number, + scale: Pair, +}; + +export type AnimationPromise = Promise & Cancelable; + +export interface Pair { + x: number; + y: number; +} + +export interface Cancelable { + cancel(): void; +} + +export interface PropertyAnimation { + target: View; + property: string; + value: any; + duration?: number; + delay?: number; + iterations?: number; + curve?: any; +} + +export interface PropertyAnimationInfo extends PropertyAnimation { + _propertyResetCallback?: any; + _originalValue?: any; +} + +export interface AnimationDefinition { + target?: View; + opacity?: number; + backgroundColor?: Color; + translate?: Pair; + scale?: Pair; + height?: PercentLength | string; + width?: PercentLength | string; + rotate?: number; + duration?: number; + delay?: number; + iterations?: number; + curve?: any; +} + +export interface AnimationDefinitionInternal extends AnimationDefinition { + valueSource?: "animation" | "keyframe"; +} + +export interface IOSView extends View { + _suspendPresentationLayerUpdates(); + _resumePresentationLayerUpdates(); + _isPresentationLayerUpdateSuspeneded(); +} diff --git a/nativescript-core/ui/animation/animation.android.ts b/nativescript-core/ui/animation/animation.android.ts index 3aef429c9..ee4097b29 100644 --- a/nativescript-core/ui/animation/animation.android.ts +++ b/nativescript-core/ui/animation/animation.android.ts @@ -1,22 +1,22 @@ -// Definitions. -import { AnimationDefinition, AnimationPromise } from "."; +// Types. +import { AnimationDefinitionInternal, AnimationPromise, PropertyAnimation } from "./animation-common"; import { View } from "../core/view"; -import { AnimationBase, Properties, PropertyAnimation, CubicBezierAnimationCurve, Color, traceWrite, traceEnabled, traceCategories, traceType } from "./animation-common"; +// Requires +import { + AnimationBase, Properties, CubicBezierAnimationCurve, Color, traceWrite, + traceEnabled, traceCategories, traceType +} from "./animation-common"; import { opacityProperty, backgroundColorProperty, rotateProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, heightProperty, widthProperty, PercentLength } from "../styling/style-properties"; - import { layout } from "../../utils/utils"; import { device, screen } from "../../platform"; import lazy from "../../utils/lazy"; -export * from "./animation-common"; -interface AnimationDefinitionInternal extends AnimationDefinition { - valueSource?: "animation" | "keyframe"; -} +export * from "./animation-common"; let argbEvaluator: android.animation.ArgbEvaluator; function ensureArgbEvaluator() { diff --git a/nativescript-core/ui/animation/animation.ios.ts b/nativescript-core/ui/animation/animation.ios.ts index 29f854055..2cf0c31cb 100644 --- a/nativescript-core/ui/animation/animation.ios.ts +++ b/nativescript-core/ui/animation/animation.ios.ts @@ -1,7 +1,15 @@ -import { AnimationDefinition, AnimationPromise } from "."; +// Types +import { + AnimationDefinitionInternal, AnimationPromise, IOSView, + PropertyAnimation, PropertyAnimationInfo +} from "./animation-common"; import { View } from "../core/view"; -import { AnimationBase, Properties, PropertyAnimation, CubicBezierAnimationCurve, traceWrite, traceEnabled, traceCategories, traceType } from "./animation-common"; +// Requires +import { + AnimationBase, Properties, CubicBezierAnimationCurve, + traceWrite, traceEnabled, traceCategories, traceType +} from "./animation-common"; import { opacityProperty, backgroundColorProperty, rotateProperty, translateXProperty, translateYProperty, scaleXProperty, scaleYProperty, @@ -26,21 +34,6 @@ class AnimationInfo { public delay: number; } -interface PropertyAnimationInfo extends PropertyAnimation { - _propertyResetCallback?: any; - _originalValue?: any; -} - -interface AnimationDefinitionInternal extends AnimationDefinition { - valueSource?: "animation" | "keyframe"; -} - -interface IOSView extends View { - _suspendPresentationLayerUpdates(); - _resumePresentationLayerUpdates(); - _isPresentationLayerUpdateSuspeneded(); -} - class AnimationDelegateImpl extends NSObject implements CAAnimationDelegate { public nextAnimation: Function; diff --git a/nativescript-core/ui/core/bindable/bindable-resources.ts b/nativescript-core/ui/core/bindable/bindable-resources.ts new file mode 100644 index 000000000..ef9aae8c0 --- /dev/null +++ b/nativescript-core/ui/core/bindable/bindable-resources.ts @@ -0,0 +1,9 @@ +let resources: any = {}; + +export function get() { + return resources; +} + +export function set(res: any) { + resources = res; +} \ No newline at end of file diff --git a/nativescript-core/ui/core/bindable/bindable.ts b/nativescript-core/ui/core/bindable/bindable.ts index 9dd65d386..46428f254 100644 --- a/nativescript-core/ui/core/bindable/bindable.ts +++ b/nativescript-core/ui/core/bindable/bindable.ts @@ -1,6 +1,8 @@ +// Types import { BindingOptions } from "."; import { ViewBase } from "../view-base"; +// Requires import { unsetValue } from "../properties"; import { Observable, WrappedValue, PropertyChangeData, EventData } from "../../../data/observable"; import { addWeakEventListener, removeWeakEventListener } from "../weak-event-listener"; @@ -16,8 +18,7 @@ import { messageType as traceMessageType } from "../../../trace"; import * as types from "../../../utils/types"; - -import * as applicationCommon from "../../../application/application-common"; +import * as bindableResources from "./bindable-resources"; import * as polymerExpressions from "../../../js-libs/polymer-expressions"; export { @@ -368,7 +369,7 @@ export class Binding { let context = this.source && this.source.get && this.source.get() || global; let model = {}; let addedProps = []; - const resources = applicationCommon.getResources(); + const resources = bindableResources.get(); for (let prop in resources) { if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) { context[prop] = resources[prop]; diff --git a/nativescript-core/ui/core/view-base/view-base.d.ts b/nativescript-core/ui/core/view-base/view-base.d.ts index 28023a771..81723081f 100644 --- a/nativescript-core/ui/core/view-base/view-base.d.ts +++ b/nativescript-core/ui/core/view-base/view-base.d.ts @@ -439,6 +439,11 @@ export abstract class ViewBase extends Observable { */ _isStyleScopeHost: boolean; + /** + * @private + */ + public _layoutParent(): void; + /** * Determines the depth of suspended updates. * When the value is 0 the current property updates are not batched nor scoped and must be immediately applied. diff --git a/nativescript-core/ui/core/view-base/view-base.ts b/nativescript-core/ui/core/view-base/view-base.ts index 89dc23607..8e3e730be 100644 --- a/nativescript-core/ui/core/view-base/view-base.ts +++ b/nativescript-core/ui/core/view-base/view-base.ts @@ -377,6 +377,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition this._emit("unloaded"); } + public _layoutParent() { + if (this.parent) { + this.parent._layoutParent(); + } + } + public _suspendNativeUpdates(type: SuspendType): void { if (type) { this._suspendNativeUpdatesCount = this._suspendNativeUpdatesCount | type; diff --git a/nativescript-core/ui/core/view/view-common.ts b/nativescript-core/ui/core/view/view-common.ts index 1618a2090..ae4373866 100644 --- a/nativescript-core/ui/core/view/view-common.ts +++ b/nativescript-core/ui/core/view/view-common.ts @@ -8,6 +8,7 @@ import { booleanConverter, EventData, getEventOrGestureName, InheritedProperty, layout, Property, ShowModalOptions, traceCategories, traceEnabled, traceWrite, ViewBase } from "../view-base"; +import { ViewHelper } from "./view-helper"; import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from "../../styling/style-properties"; @@ -806,193 +807,19 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { public abstract layoutNativeView(left: number, top: number, right: number, bottom: number): void; public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number { - let result = size; - switch (specMode) { - case layout.UNSPECIFIED: - result = Math.ceil(size); - break; - - case layout.AT_MOST: - if (specSize < size) { - result = Math.ceil(specSize) | layout.MEASURED_STATE_TOO_SMALL; - } - break; - - case layout.EXACTLY: - result = Math.ceil(specSize); - break; - } - - return result | (childMeasuredState & layout.MEASURED_STATE_MASK); + return ViewHelper.resolveSizeAndState(size, specSize, specMode, childMeasuredState); } public static combineMeasuredStates(curState: number, newState): number { - return curState | newState; + return ViewHelper.combineMeasuredStates(curState, newState); } public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame: boolean = true): void { - if (!child || child.isCollapsed) { - return; - } - - let childStyle = child.style; - - let childTop: number; - let childLeft: number; - - let childWidth = child.getMeasuredWidth(); - let childHeight = child.getMeasuredHeight(); - - let effectiveMarginTop = child.effectiveMarginTop; - let effectiveMarginBottom = child.effectiveMarginBottom; - - let vAlignment: VerticalAlignment; - if (child.effectiveHeight >= 0 && childStyle.verticalAlignment === "stretch") { - vAlignment = "middle"; - } - else { - vAlignment = childStyle.verticalAlignment; - } - - switch (vAlignment) { - case "top": - childTop = top + effectiveMarginTop; - break; - - case "middle": - childTop = top + (bottom - top - childHeight + (effectiveMarginTop - effectiveMarginBottom)) / 2; - break; - - case "bottom": - childTop = bottom - childHeight - effectiveMarginBottom; - break; - - case "stretch": - default: - childTop = top + effectiveMarginTop; - childHeight = bottom - top - (effectiveMarginTop + effectiveMarginBottom); - break; - } - - let effectiveMarginLeft = child.effectiveMarginLeft; - let effectiveMarginRight = child.effectiveMarginRight; - - let hAlignment: HorizontalAlignment; - if (child.effectiveWidth >= 0 && childStyle.horizontalAlignment === "stretch") { - hAlignment = "center"; - } - else { - hAlignment = childStyle.horizontalAlignment; - } - - switch (hAlignment) { - case "left": - childLeft = left + effectiveMarginLeft; - break; - - case "center": - childLeft = left + (right - left - childWidth + (effectiveMarginLeft - effectiveMarginRight)) / 2; - break; - - case "right": - childLeft = right - childWidth - effectiveMarginRight; - break; - - case "stretch": - default: - childLeft = left + effectiveMarginLeft; - childWidth = right - left - (effectiveMarginLeft + effectiveMarginRight); - break; - } - - let childRight = Math.round(childLeft + childWidth); - let childBottom = Math.round(childTop + childHeight); - childLeft = Math.round(childLeft); - childTop = Math.round(childTop); - - if (traceEnabled()) { - traceWrite(child.parent + " :layoutChild: " + child + " " + childLeft + ", " + childTop + ", " + childRight + ", " + childBottom, traceCategories.Layout); - } - - child.layout(childLeft, childTop, childRight, childBottom, setFrame); + ViewHelper.layoutChild(parent, child, left, top, right, bottom); } public static measureChild(parent: ViewCommon, child: ViewCommon, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number } { - let measureWidth = 0; - let measureHeight = 0; - - if (child && !child.isCollapsed) { - - const widthSpec = parent ? parent._currentWidthMeasureSpec : widthMeasureSpec; - const heightSpec = parent ? parent._currentHeightMeasureSpec : heightMeasureSpec; - - const width = layout.getMeasureSpecSize(widthSpec); - const widthMode = layout.getMeasureSpecMode(widthSpec); - - const height = layout.getMeasureSpecSize(heightSpec); - const heightMode = layout.getMeasureSpecMode(heightSpec); - - child._updateEffectiveLayoutValues(width, widthMode, height, heightMode); - - const style = child.style; - const horizontalMargins = child.effectiveMarginLeft + child.effectiveMarginRight; - const verticalMargins = child.effectiveMarginTop + child.effectiveMarginBottom; - - const childWidthMeasureSpec = ViewCommon.getMeasureSpec(widthMeasureSpec, horizontalMargins, child.effectiveWidth, style.horizontalAlignment === "stretch"); - const childHeightMeasureSpec = ViewCommon.getMeasureSpec(heightMeasureSpec, verticalMargins, child.effectiveHeight, style.verticalAlignment === "stretch"); - - if (traceEnabled()) { - traceWrite(`${child.parent} :measureChild: ${child} ${layout.measureSpecToString(childWidthMeasureSpec)}, ${layout.measureSpecToString(childHeightMeasureSpec)}}`, traceCategories.Layout); - } - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - measureWidth = Math.round(child.getMeasuredWidth() + horizontalMargins); - measureHeight = Math.round(child.getMeasuredHeight() + verticalMargins); - } - - return { measuredWidth: measureWidth, measuredHeight: measureHeight }; - } - - private static getMeasureSpec(parentSpec: number, margins: number, childLength: number, stretched: boolean): number { - const parentLength = layout.getMeasureSpecSize(parentSpec); - const parentSpecMode = layout.getMeasureSpecMode(parentSpec); - - let resultSize: number; - let resultMode: number; - - // We want a specific size... let be it. - if (childLength >= 0) { - // If mode !== UNSPECIFIED we take the smaller of parentLength and childLength - // Otherwise we will need to clip the view but this is not possible in all Android API levels. - // TODO: remove Math.min(parentLength, childLength) - resultSize = parentSpecMode === layout.UNSPECIFIED ? childLength : Math.min(parentLength, childLength); - resultMode = layout.EXACTLY; - } - else { - switch (parentSpecMode) { - // Parent has imposed an exact size on us - case layout.EXACTLY: - resultSize = Math.max(0, parentLength - margins); - // if stretched - nativeView wants to be our size. So be it. - // else - nativeView wants to determine its own size. It can't be bigger than us. - resultMode = stretched ? layout.EXACTLY : layout.AT_MOST; - break; - - // Parent has imposed a maximum size on us - case layout.AT_MOST: - resultSize = Math.max(0, parentLength - margins); - resultMode = layout.AT_MOST; - break; - - // Equivalent to measure with Infinity. - case layout.UNSPECIFIED: - resultSize = 0; - resultMode = layout.UNSPECIFIED; - break; - } - } - - return layout.makeMeasureSpec(resultSize, resultMode); + return ViewHelper.measureChild(parent, child, widthMeasureSpec, heightMeasureSpec); } _setCurrentMeasureSpecs(widthMeasureSpec: number, heightMeasureSpec: number): boolean { diff --git a/nativescript-core/ui/core/view/view-helper/package.json b/nativescript-core/ui/core/view/view-helper/package.json new file mode 100644 index 000000000..0561420d6 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/package.json @@ -0,0 +1,5 @@ +{ + "name": "view-helper", + "main": "view-helper", + "types": "view-helper.d.ts" +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper-common.ts b/nativescript-core/ui/core/view/view-helper/view-helper-common.ts new file mode 100644 index 000000000..f41213e65 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper-common.ts @@ -0,0 +1,207 @@ +// Types +import { View as ViewDefinition } from ".."; +import { + HorizontalAlignment as HorizontalAlignmentDefinition, + VerticalAlignment as VerticalAlignmentDefinition +} from "../../../styling/style-properties"; + +// Requires +import { layout } from "../../../../utils/utils"; +import { + isEnabled as traceEnabled, + categories as traceCategories, + write as traceWrite +} from "../../../../trace"; + +export class ViewHelper { + + public static measureChild(parent: ViewDefinition, child: ViewDefinition, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number } { + let measureWidth = 0; + let measureHeight = 0; + + if (child && !child.isCollapsed) { + + const widthSpec = parent ? parent._currentWidthMeasureSpec : widthMeasureSpec; + const heightSpec = parent ? parent._currentHeightMeasureSpec : heightMeasureSpec; + + const width = layout.getMeasureSpecSize(widthSpec); + const widthMode = layout.getMeasureSpecMode(widthSpec); + + const height = layout.getMeasureSpecSize(heightSpec); + const heightMode = layout.getMeasureSpecMode(heightSpec); + + child._updateEffectiveLayoutValues(width, widthMode, height, heightMode); + + const style = child.style; + const horizontalMargins = child.effectiveMarginLeft + child.effectiveMarginRight; + const verticalMargins = child.effectiveMarginTop + child.effectiveMarginBottom; + + const childWidthMeasureSpec = ViewHelper.getMeasureSpec(widthMeasureSpec, horizontalMargins, child.effectiveWidth, style.horizontalAlignment === "stretch"); + const childHeightMeasureSpec = ViewHelper.getMeasureSpec(heightMeasureSpec, verticalMargins, child.effectiveHeight, style.verticalAlignment === "stretch"); + + if (traceEnabled()) { + traceWrite(`${child.parent} :measureChild: ${child} ${layout.measureSpecToString(childWidthMeasureSpec)}, ${layout.measureSpecToString(childHeightMeasureSpec)}}`, traceCategories.Layout); + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + measureWidth = Math.round(child.getMeasuredWidth() + horizontalMargins); + measureHeight = Math.round(child.getMeasuredHeight() + verticalMargins); + } + + return { measuredWidth: measureWidth, measuredHeight: measureHeight }; + } + + public static layoutChild(parent: ViewDefinition, child: ViewDefinition, left: number, top: number, right: number, bottom: number, setFrame: boolean = true): void { + if (!child || child.isCollapsed) { + return; + } + + let childStyle = child.style; + + let childTop: number; + let childLeft: number; + + let childWidth = child.getMeasuredWidth(); + let childHeight = child.getMeasuredHeight(); + + let effectiveMarginTop = child.effectiveMarginTop; + let effectiveMarginBottom = child.effectiveMarginBottom; + + let vAlignment: VerticalAlignmentDefinition; + if (child.effectiveHeight >= 0 && childStyle.verticalAlignment === "stretch") { + vAlignment = "middle"; + } + else { + vAlignment = childStyle.verticalAlignment; + } + + switch (vAlignment) { + case "top": + childTop = top + effectiveMarginTop; + break; + + case "middle": + childTop = top + (bottom - top - childHeight + (effectiveMarginTop - effectiveMarginBottom)) / 2; + break; + + case "bottom": + childTop = bottom - childHeight - effectiveMarginBottom; + break; + + case "stretch": + default: + childTop = top + effectiveMarginTop; + childHeight = bottom - top - (effectiveMarginTop + effectiveMarginBottom); + break; + } + + let effectiveMarginLeft = child.effectiveMarginLeft; + let effectiveMarginRight = child.effectiveMarginRight; + + let hAlignment: HorizontalAlignmentDefinition; + if (child.effectiveWidth >= 0 && childStyle.horizontalAlignment === "stretch") { + hAlignment = "center"; + } + else { + hAlignment = childStyle.horizontalAlignment; + } + + switch (hAlignment) { + case "left": + childLeft = left + effectiveMarginLeft; + break; + + case "center": + childLeft = left + (right - left - childWidth + (effectiveMarginLeft - effectiveMarginRight)) / 2; + break; + + case "right": + childLeft = right - childWidth - effectiveMarginRight; + break; + + case "stretch": + default: + childLeft = left + effectiveMarginLeft; + childWidth = right - left - (effectiveMarginLeft + effectiveMarginRight); + break; + } + + let childRight = Math.round(childLeft + childWidth); + let childBottom = Math.round(childTop + childHeight); + childLeft = Math.round(childLeft); + childTop = Math.round(childTop); + + if (traceEnabled()) { + traceWrite(child.parent + " :layoutChild: " + child + " " + childLeft + ", " + childTop + ", " + childRight + ", " + childBottom, traceCategories.Layout); + } + + child.layout(childLeft, childTop, childRight, childBottom, setFrame); + } + + public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number { + let result = size; + switch (specMode) { + case layout.UNSPECIFIED: + result = Math.ceil(size); + break; + + case layout.AT_MOST: + if (specSize < size) { + result = Math.ceil(specSize) | layout.MEASURED_STATE_TOO_SMALL; + } + break; + + case layout.EXACTLY: + result = Math.ceil(specSize); + break; + } + + return result | (childMeasuredState & layout.MEASURED_STATE_MASK); + } + + public static combineMeasuredStates(curState: number, newState): number { + return curState | newState; + } + + private static getMeasureSpec(parentSpec: number, margins: number, childLength: number, stretched: boolean): number { + const parentLength = layout.getMeasureSpecSize(parentSpec); + const parentSpecMode = layout.getMeasureSpecMode(parentSpec); + + let resultSize: number; + let resultMode: number; + + // We want a specific size... let be it. + if (childLength >= 0) { + // If mode !== UNSPECIFIED we take the smaller of parentLength and childLength + // Otherwise we will need to clip the view but this is not possible in all Android API levels. + // TODO: remove Math.min(parentLength, childLength) + resultSize = parentSpecMode === layout.UNSPECIFIED ? childLength : Math.min(parentLength, childLength); + resultMode = layout.EXACTLY; + } + else { + switch (parentSpecMode) { + // Parent has imposed an exact size on us + case layout.EXACTLY: + resultSize = Math.max(0, parentLength - margins); + // if stretched - nativeView wants to be our size. So be it. + // else - nativeView wants to determine its own size. It can't be bigger than us. + resultMode = stretched ? layout.EXACTLY : layout.AT_MOST; + break; + + // Parent has imposed a maximum size on us + case layout.AT_MOST: + resultSize = Math.max(0, parentLength - margins); + resultMode = layout.AT_MOST; + break; + + // Equivalent to measure with Infinity. + case layout.UNSPECIFIED: + resultSize = 0; + resultMode = layout.UNSPECIFIED; + break; + } + } + + return layout.makeMeasureSpec(resultSize, resultMode); + } +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.android.ts b/nativescript-core/ui/core/view/view-helper/view-helper.android.ts new file mode 100644 index 000000000..d95f1c4ff --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.android.ts @@ -0,0 +1 @@ +export * from "./view-helper-common"; diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.d.ts b/nativescript-core/ui/core/view/view-helper/view-helper.d.ts new file mode 100644 index 000000000..377b5474d --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.d.ts @@ -0,0 +1,63 @@ +import { View } from ".."; + +export class ViewHelper { + /** + * Measure a child by taking into account its margins and a given measureSpecs. + * @param parent This parameter is not used. You can pass null. + * @param child The view to be measured. + * @param measuredWidth The measured width that the parent layout specifies for this view. + * @param measuredHeight The measured height that the parent layout specifies for this view. + */ + public static measureChild(parent: View, child: View, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number }; + + /** + * Layout a child by taking into account its margins, horizontal and vertical alignments and a given bounds. + * @param parent This parameter is not used. You can pass null. + * @param left Left position, relative to parent + * @param top Top position, relative to parent + * @param right Right position, relative to parent + * @param bottom Bottom position, relative to parent + */ + public static layoutChild(parent: View, child: View, left: number, top: number, right: number, bottom: number): void; + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, + * with the resolved size in the MEASURED_SIZE_MASK bits and + * optionally the bit MEASURED_STATE_TOO_SMALL set if the resulting + * size is smaller than the size the view wants to be. + */ + public static resolveSizeAndState(size: number, specSize: number, specMode: number, childMeasuredState: number): number; + + public static combineMeasuredStates(curState: number, newState): number; +} + +export namespace ios { + /** + * String value used when hooking to traitCollectionColorAppearanceChangedEvent event. + */ + export const traitCollectionColorAppearanceChangedEvent: string; + + /** + * Returns a view with viewController or undefined if no such found along the view's parent chain. + * @param view The view form which to start the search. + */ + export function getParentWithViewController(view: View): View + export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void + export function updateConstraints(controller: any /* UIViewController */, owner: View): void; + export function layoutView(controller: any /* UIViewController */, owner: View): void; + export function getPositionFromFrame(frame: any /* CGRect */): { left, top, right, bottom }; + export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): any /* CGRect */; + export function shrinkToSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */; + export function expandBeyondSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */; + export class UILayoutViewController { + public static initWithOwner(owner: WeakRef): UILayoutViewController; + } + export class UIAdaptivePresentationControllerDelegateImp { + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp; + } + export class UIPopoverPresentationControllerDelegateImp { + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp; + } +} diff --git a/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts b/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts new file mode 100644 index 000000000..99a8e9838 --- /dev/null +++ b/nativescript-core/ui/core/view/view-helper/view-helper.ios.ts @@ -0,0 +1,365 @@ +// Types +import { View } from ".."; + +// Requires +import { ViewHelper } from "./view-helper-common"; +import { + ios as iosUtils, + layout +} from "../../../../utils/utils"; +import { + isEnabled as traceEnabled, + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../../../../trace"; + +export * from "./view-helper-common"; + +const majorVersion = iosUtils.MajorVersion; + +export namespace ios { + export const traitCollectionColorAppearanceChangedEvent = "traitCollectionColorAppearanceChanged"; + + export function getParentWithViewController(view: View): View { + while (view && !view.viewController) { + view = view.parent as View; + } + + // Note: Might return undefined if no parent with viewController is found + return view; + } + + export function updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void { + if (majorVersion <= 10) { + owner._automaticallyAdjustsScrollViewInsets = false; + // This API is deprecated, but has no alternative for <= iOS 10 + // Defaults to true and results to appliyng the insets twice together with our logic + // for iOS 11+ we use the contentInsetAdjustmentBehavior property in scrollview + // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621372-automaticallyadjustsscrollviewin + controller.automaticallyAdjustsScrollViewInsets = false; + } + } + + export function updateConstraints(controller: UIViewController, owner: View): void { + if (majorVersion <= 10) { + const layoutGuide = initLayoutGuide(controller); + (controller.view).safeAreaLayoutGuide = layoutGuide; + } + } + + function initLayoutGuide(controller: UIViewController) { + const rootView = controller.view; + const layoutGuide = UILayoutGuide.alloc().init(); + rootView.addLayoutGuide(layoutGuide); + NSLayoutConstraint.activateConstraints([ + layoutGuide.topAnchor.constraintEqualToAnchor(controller.topLayoutGuide.bottomAnchor), + layoutGuide.bottomAnchor.constraintEqualToAnchor(controller.bottomLayoutGuide.topAnchor), + layoutGuide.leadingAnchor.constraintEqualToAnchor(rootView.leadingAnchor), + layoutGuide.trailingAnchor.constraintEqualToAnchor(rootView.trailingAnchor) + ]); + + return layoutGuide; + } + + export function layoutView(controller: UIViewController, owner: View): void { + let layoutGuide = controller.view.safeAreaLayoutGuide; + if (!layoutGuide) { + traceWrite(`safeAreaLayoutGuide during layout of ${owner}. Creating fallback constraints, but layout might be wrong.`, + traceCategories.Layout, traceMessageType.error); + + layoutGuide = initLayoutGuide(controller); + } + const safeArea = layoutGuide.layoutFrame; + let position = ios.getPositionFromFrame(safeArea); + const safeAreaSize = safeArea.size; + + const hasChildViewControllers = controller.childViewControllers.count > 0; + if (hasChildViewControllers) { + const fullscreen = controller.view.frame; + position = ios.getPositionFromFrame(fullscreen); + } + + const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width)); + const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height)); + + const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY); + const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.EXACTLY); + + ViewHelper.measureChild(null, owner, widthSpec, heightSpec); + ViewHelper.layoutChild(null, owner, position.left, position.top, position.right, position.bottom); + + if (owner.parent) { + owner.parent._layoutParent(); + } + } + + export function getPositionFromFrame(frame: CGRect): { left, top, right, bottom } { + const left = layout.round(layout.toDevicePixels(frame.origin.x)); + const top = layout.round(layout.toDevicePixels(frame.origin.y)); + const right = layout.round(layout.toDevicePixels(frame.origin.x + frame.size.width)); + const bottom = layout.round(layout.toDevicePixels(frame.origin.y + frame.size.height)); + + return { left, right, top, bottom }; + } + + export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): CGRect { + insets = insets || { left: 0, top: 0, right: 0, bottom: 0 }; + + const left = layout.toDeviceIndependentPixels(position.left + insets.left); + const top = layout.toDeviceIndependentPixels(position.top + insets.top); + const width = layout.toDeviceIndependentPixels(position.right - position.left - insets.left - insets.right); + const height = layout.toDeviceIndependentPixels(position.bottom - position.top - insets.top - insets.bottom); + + return CGRectMake(left, top, width, height); + } + + export function shrinkToSafeArea(view: View, frame: CGRect): CGRect { + const insets = view.getSafeAreaInsets(); + if (insets.left || insets.top) { + const position = ios.getPositionFromFrame(frame); + const adjustedFrame = ios.getFrameFromPosition(position, insets); + + if (traceEnabled()) { + traceWrite(this + " :shrinkToSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); + } + + return adjustedFrame; + } + + return null; + } + + export function expandBeyondSafeArea(view: View, frame: CGRect): CGRect { + const availableSpace = getAvailableSpaceFromParent(view, frame); + const safeArea = availableSpace.safeArea; + const fullscreen = availableSpace.fullscreen; + const inWindow = availableSpace.inWindow; + + const position = ios.getPositionFromFrame(frame); + const safeAreaPosition = ios.getPositionFromFrame(safeArea); + const fullscreenPosition = ios.getPositionFromFrame(fullscreen); + const inWindowPosition = ios.getPositionFromFrame(inWindow); + + const adjustedPosition = position; + + if (position.left && inWindowPosition.left <= safeAreaPosition.left) { + adjustedPosition.left = fullscreenPosition.left; + } + + if (position.top && inWindowPosition.top <= safeAreaPosition.top) { + adjustedPosition.top = fullscreenPosition.top; + } + + if (inWindowPosition.right < fullscreenPosition.right && inWindowPosition.right >= safeAreaPosition.right + fullscreenPosition.left) { + adjustedPosition.right += fullscreenPosition.right - inWindowPosition.right; + } + + if (inWindowPosition.bottom < fullscreenPosition.bottom && inWindowPosition.bottom >= safeAreaPosition.bottom + fullscreenPosition.top) { + adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; + } + + const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); + + if (traceEnabled()) { + traceWrite(view + " :expandBeyondSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); + } + + return adjustedFrame; + } + + function getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect, fullscreen: CGRect, inWindow: CGRect } { + if (!view) { + return; + } + + let scrollView = null; + let viewControllerView = null; + + if (view.viewController) { + viewControllerView = view.viewController.view; + } else { + let parent = view.parent as View; + while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) { + parent = parent.parent as View; + } + + if (parent.nativeViewProtected instanceof UIScrollView) { + scrollView = parent.nativeViewProtected; + } else if (parent.viewController) { + viewControllerView = parent.viewController.view; + } + } + + let fullscreen = null; + let safeArea = null; + + if (viewControllerView) { + safeArea = viewControllerView.safeAreaLayoutGuide.layoutFrame; + fullscreen = viewControllerView.frame; + } + else if (scrollView) { + const insets = scrollView.safeAreaInsets; + safeArea = CGRectMake(insets.left, insets.top, scrollView.contentSize.width - insets.left - insets.right, scrollView.contentSize.height - insets.top - insets.bottom); + fullscreen = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); + } + + const locationInWindow = view.getLocationInWindow(); + let inWindowLeft = locationInWindow.x; + let inWindowTop = locationInWindow.y; + + if (scrollView) { + inWindowLeft += scrollView.contentOffset.x; + inWindowTop += scrollView.contentOffset.y; + } + + const inWindow = CGRectMake(inWindowLeft, inWindowTop, frame.size.width, frame.size.height); + + return { safeArea: safeArea, fullscreen: fullscreen, inWindow: inWindow }; + } + + export class UILayoutViewController extends UIViewController { + public owner: WeakRef; + + public static initWithOwner(owner: WeakRef): UILayoutViewController { + const controller = UILayoutViewController.new(); + controller.owner = owner; + + return controller; + } + + public viewDidLoad(): void { + super.viewDidLoad(); + + // Unify translucent and opaque bars layout + // this.edgesForExtendedLayout = UIRectEdgeBottom; + this.extendedLayoutIncludesOpaqueBars = true; + } + + public viewWillLayoutSubviews(): void { + super.viewWillLayoutSubviews(); + const owner = this.owner.get(); + if (owner) { + updateConstraints(this, owner); + } + } + + public viewDidLayoutSubviews(): void { + super.viewDidLayoutSubviews(); + const owner = this.owner.get(); + if (owner) { + if (majorVersion >= 11) { + // Handle nested UILayoutViewController safe area application. + // Currently, UILayoutViewController can be nested only in a TabView. + // The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout). + const tabViewItem = owner.parent; + const tabView = tabViewItem && tabViewItem.parent; + let parent = tabView && tabView.parent; + + // Handle Angular scenario where TabView is in a ProxyViewContainer + // It is possible to wrap components in ProxyViewContainers indefinitely + // Not using instanceof ProxyViewContainer to avoid circular dependency + // TODO: Try moving UILayoutViewController out of view module + while (parent && !parent.nativeViewProtected) { + parent = parent.parent; + } + + if (parent) { + const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top; + const currentInsetsTop = this.view.safeAreaInsets.top; + const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0); + + const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom; + const currentInsetsBottom = this.view.safeAreaInsets.bottom; + const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0); + + if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) { + const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 }); + this.additionalSafeAreaInsets = additionalInsets; + } + } + } + + layoutView(this, owner); + } + } + + public viewWillAppear(animated: boolean): void { + super.viewWillAppear(animated); + const owner = this.owner.get(); + if (!owner) { + return; + } + + updateAutoAdjustScrollInsets(this, owner); + + if (!owner.parent) { + owner.callLoaded(); + } + } + + public viewDidDisappear(animated: boolean): void { + super.viewDidDisappear(animated); + const owner = this.owner.get(); + if (owner && !owner.parent) { + owner.callUnloaded(); + } + } + + // Mind implementation for other controllers + public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { + super.traitCollectionDidChange(previousTraitCollection); + + if (majorVersion >= 13) { + const owner = this.owner.get(); + if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { + owner.notify({ eventName: traitCollectionColorAppearanceChangedEvent, object: owner }); + } + } + } + } + + export class UIAdaptivePresentationControllerDelegateImp extends NSObject implements UIAdaptivePresentationControllerDelegate { + public static ObjCProtocols = [UIAdaptivePresentationControllerDelegate]; + + private owner: WeakRef; + private closedCallback: Function; + + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp { + const instance = super.new(); + instance.owner = owner; + instance.closedCallback = whenClosedCallback; + + return instance; + } + + public presentationControllerDidDismiss(presentationController: UIPresentationController) { + const owner = this.owner.get(); + if (owner && typeof this.closedCallback === "function") { + this.closedCallback(); + } + } + } + + export class UIPopoverPresentationControllerDelegateImp extends NSObject implements UIPopoverPresentationControllerDelegate { + public static ObjCProtocols = [UIPopoverPresentationControllerDelegate]; + + private owner: WeakRef; + private closedCallback: Function; + + public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp { + const instance = super.new(); + instance.owner = owner; + instance.closedCallback = whenClosedCallback; + + return instance; + } + + public popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) { + const owner = this.owner.get(); + if (owner && typeof this.closedCallback === "function") { + this.closedCallback(); + } + } + } +} \ No newline at end of file diff --git a/nativescript-core/ui/core/view/view.ios.ts b/nativescript-core/ui/core/view/view.ios.ts index ac9cc56d2..822b160b7 100644 --- a/nativescript-core/ui/core/view/view.ios.ts +++ b/nativescript-core/ui/core/view/view.ios.ts @@ -1,12 +1,12 @@ -// Definitions. +// Types. import { Point, View as ViewDefinition, dip } from "."; -import { ViewBase } from "../view-base"; +// Requires import { ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty, traceEnabled, traceWrite, traceCategories, traceError, traceMessageType, ShowModalOptions } from "./view-common"; - +import { ios } from "./view-helper"; import { ios as iosBackground, Background } from "../../styling/background"; import { ios as iosUtils } from "../../../utils/utils"; import { @@ -19,6 +19,7 @@ import { import { profile } from "../../../profiling"; export * from "./view-common"; +export { ios }; const PFLAG_FORCE_LAYOUT = 1; const PFLAG_MEASURED_DIMENSION_SET = 1 << 1; @@ -26,7 +27,7 @@ const PFLAG_LAYOUT_REQUIRED = 1 << 2; const majorVersion = iosUtils.MajorVersion; -export class View extends ViewCommon { +export class View extends ViewCommon implements ViewDefinition { nativeViewProtected: UIView; viewController: UIViewController; private _popoverPresentationDelegate: ios.UIPopoverPresentationControllerDelegateImp; @@ -215,6 +216,21 @@ export class View extends ViewCommon { this._setNativeViewFrame(nativeView, frame); } + public _layoutParent() { + if (this.nativeViewProtected) { + const frame = this.nativeViewProtected.frame; + const origin = frame.origin; + const size = frame.size; + const left = layout.toDevicePixels(origin.x); + const top = layout.toDevicePixels(origin.y); + const width = layout.toDevicePixels(size.width); + const height = layout.toDevicePixels(size.height); + this._setLayoutFlags(left, top, width + left, height + top); + } + + super._layoutParent(); + } + public _setLayoutFlags(left: number, top: number, right: number, bottom: number): void { const width = right - left; const height = bottom - top; @@ -399,11 +415,11 @@ export class View extends ViewCommon { this._setupAsRootView({}); - super._showNativeModalView(parentWithController, options); + super._showNativeModalView(parentWithController, options); let controller = this.viewController; if (!controller) { const nativeView = this.ios || this.nativeViewProtected; - controller = ios.UILayoutViewController.initWithOwner(new WeakRef(this)); + controller = ios.UILayoutViewController.initWithOwner(new WeakRef(this)); if (nativeView instanceof UIView) { controller.view.addSubview(nativeView); @@ -654,7 +670,7 @@ export class View extends ViewCommon { private _setupPopoverControllerDelegate(controller: UIViewController, parent: View) { const popoverPresentationController = controller.popoverPresentationController; this._popoverPresentationDelegate = ios.UIPopoverPresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback); - popoverPresentationController.delegate = this._popoverPresentationDelegate; + popoverPresentationController.delegate = this._popoverPresentationDelegate; const view = parent.nativeViewProtected; // Note: sourceView and sourceRect are needed to specify the anchor location for the popover. // Note: sourceView should be the button triggering the modal. If it the Page the popover might appear "behind" the page content @@ -664,7 +680,7 @@ export class View extends ViewCommon { private _setupAdaptiveControllerDelegate(controller: UIViewController) { this._adaptivePresentationDelegate = ios.UIAdaptivePresentationControllerDelegateImp.initWithOwnerAndCallback(new WeakRef(this), this._closeModalCallback); - controller.presentationController.delegate = this._adaptivePresentationDelegate; + controller.presentationController.delegate = this._adaptivePresentationDelegate; } } View.prototype._nativeBackgroundState = "unset"; @@ -722,366 +738,3 @@ export class CustomLayoutView extends ContainerView { } } } - -export namespace ios { - export const traitCollectionColorAppearanceChangedEvent = "traitCollectionColorAppearanceChanged"; - - export function getParentWithViewController(view: View): View { - while (view && !view.viewController) { - view = view.parent as View; - } - - // Note: Might return undefined if no parent with viewController is found - return view; - } - - export function updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void { - if (majorVersion <= 10) { - owner._automaticallyAdjustsScrollViewInsets = false; - // This API is deprecated, but has no alternative for <= iOS 10 - // Defaults to true and results to appliyng the insets twice together with our logic - // for iOS 11+ we use the contentInsetAdjustmentBehavior property in scrollview - // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621372-automaticallyadjustsscrollviewin - controller.automaticallyAdjustsScrollViewInsets = false; - } - } - - export function updateConstraints(controller: UIViewController, owner: View): void { - if (majorVersion <= 10) { - const layoutGuide = initLayoutGuide(controller); - (controller.view).safeAreaLayoutGuide = layoutGuide; - } - } - - function initLayoutGuide(controller: UIViewController) { - const rootView = controller.view; - const layoutGuide = UILayoutGuide.alloc().init(); - rootView.addLayoutGuide(layoutGuide); - NSLayoutConstraint.activateConstraints([ - layoutGuide.topAnchor.constraintEqualToAnchor(controller.topLayoutGuide.bottomAnchor), - layoutGuide.bottomAnchor.constraintEqualToAnchor(controller.bottomLayoutGuide.topAnchor), - layoutGuide.leadingAnchor.constraintEqualToAnchor(rootView.leadingAnchor), - layoutGuide.trailingAnchor.constraintEqualToAnchor(rootView.trailingAnchor) - ]); - - return layoutGuide; - } - - export function layoutView(controller: UIViewController, owner: View): void { - let layoutGuide = controller.view.safeAreaLayoutGuide; - if (!layoutGuide) { - traceWrite(`safeAreaLayoutGuide during layout of ${owner}. Creating fallback constraints, but layout might be wrong.`, - traceCategories.Layout, traceMessageType.error); - - layoutGuide = initLayoutGuide(controller); - } - const safeArea = layoutGuide.layoutFrame; - let position = ios.getPositionFromFrame(safeArea); - const safeAreaSize = safeArea.size; - - const hasChildViewControllers = controller.childViewControllers.count > 0; - if (hasChildViewControllers) { - const fullscreen = controller.view.frame; - position = ios.getPositionFromFrame(fullscreen); - } - - const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width)); - const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height)); - - const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY); - const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.EXACTLY); - - View.measureChild(null, owner, widthSpec, heightSpec); - View.layoutChild(null, owner, position.left, position.top, position.right, position.bottom); - - layoutParent(owner.parent); - } - - export function getPositionFromFrame(frame: CGRect): { left, top, right, bottom } { - const left = layout.round(layout.toDevicePixels(frame.origin.x)); - const top = layout.round(layout.toDevicePixels(frame.origin.y)); - const right = layout.round(layout.toDevicePixels(frame.origin.x + frame.size.width)); - const bottom = layout.round(layout.toDevicePixels(frame.origin.y + frame.size.height)); - - return { left, right, top, bottom }; - } - - export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): CGRect { - insets = insets || { left: 0, top: 0, right: 0, bottom: 0 }; - - const left = layout.toDeviceIndependentPixels(position.left + insets.left); - const top = layout.toDeviceIndependentPixels(position.top + insets.top); - const width = layout.toDeviceIndependentPixels(position.right - position.left - insets.left - insets.right); - const height = layout.toDeviceIndependentPixels(position.bottom - position.top - insets.top - insets.bottom); - - return CGRectMake(left, top, width, height); - } - - export function shrinkToSafeArea(view: View, frame: CGRect): CGRect { - const insets = view.getSafeAreaInsets(); - if (insets.left || insets.top) { - const position = ios.getPositionFromFrame(frame); - const adjustedFrame = ios.getFrameFromPosition(position, insets); - - if (traceEnabled()) { - traceWrite(this + " :shrinkToSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); - } - - return adjustedFrame; - } - - return null; - } - - export function expandBeyondSafeArea(view: View, frame: CGRect): CGRect { - const availableSpace = getAvailableSpaceFromParent(view, frame); - const safeArea = availableSpace.safeArea; - const fullscreen = availableSpace.fullscreen; - const inWindow = availableSpace.inWindow; - - const position = ios.getPositionFromFrame(frame); - const safeAreaPosition = ios.getPositionFromFrame(safeArea); - const fullscreenPosition = ios.getPositionFromFrame(fullscreen); - const inWindowPosition = ios.getPositionFromFrame(inWindow); - - const adjustedPosition = position; - - if (position.left && inWindowPosition.left <= safeAreaPosition.left) { - adjustedPosition.left = fullscreenPosition.left; - } - - if (position.top && inWindowPosition.top <= safeAreaPosition.top) { - adjustedPosition.top = fullscreenPosition.top; - } - - if (inWindowPosition.right < fullscreenPosition.right && inWindowPosition.right >= safeAreaPosition.right + fullscreenPosition.left) { - adjustedPosition.right += fullscreenPosition.right - inWindowPosition.right; - } - - if (inWindowPosition.bottom < fullscreenPosition.bottom && inWindowPosition.bottom >= safeAreaPosition.bottom + fullscreenPosition.top) { - adjustedPosition.bottom += fullscreenPosition.bottom - inWindowPosition.bottom; - } - - const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top)); - - if (traceEnabled()) { - traceWrite(view + " :expandBeyondSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout); - } - - return adjustedFrame; - } - - function layoutParent(view: ViewBase): void { - if (!view) { - return; - } - - if (view instanceof View && view.nativeViewProtected) { - const frame = view.nativeViewProtected.frame; - const origin = frame.origin; - const size = frame.size; - const left = layout.toDevicePixels(origin.x); - const top = layout.toDevicePixels(origin.y); - const width = layout.toDevicePixels(size.width); - const height = layout.toDevicePixels(size.height); - view._setLayoutFlags(left, top, width + left, height + top); - } - - layoutParent(view.parent); - } - - function getAvailableSpaceFromParent(view: View, frame: CGRect): { safeArea: CGRect, fullscreen: CGRect, inWindow: CGRect } { - if (!view) { - return; - } - - let scrollView = null; - let viewControllerView = null; - - if (view.viewController) { - viewControllerView = view.viewController.view; - } else { - let parent = view.parent as View; - while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) { - parent = parent.parent as View; - } - - if (parent.nativeViewProtected instanceof UIScrollView) { - scrollView = parent.nativeViewProtected; - } else if (parent.viewController) { - viewControllerView = parent.viewController.view; - } - } - - let fullscreen = null; - let safeArea = null; - - if (viewControllerView) { - safeArea = viewControllerView.safeAreaLayoutGuide.layoutFrame; - fullscreen = viewControllerView.frame; - } - else if (scrollView) { - const insets = scrollView.safeAreaInsets; - safeArea = CGRectMake(insets.left, insets.top, scrollView.contentSize.width - insets.left - insets.right, scrollView.contentSize.height - insets.top - insets.bottom); - fullscreen = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height); - } - - const locationInWindow = view.getLocationInWindow(); - let inWindowLeft = locationInWindow.x; - let inWindowTop = locationInWindow.y; - - if (scrollView) { - inWindowLeft += scrollView.contentOffset.x; - inWindowTop += scrollView.contentOffset.y; - } - - const inWindow = CGRectMake(inWindowLeft, inWindowTop, frame.size.width, frame.size.height); - - return { safeArea: safeArea, fullscreen: fullscreen, inWindow: inWindow }; - } - - export class UILayoutViewController extends UIViewController { - public owner: WeakRef; - - public static initWithOwner(owner: WeakRef): UILayoutViewController { - const controller = UILayoutViewController.new(); - controller.owner = owner; - - return controller; - } - - public viewDidLoad(): void { - super.viewDidLoad(); - - // Unify translucent and opaque bars layout - // this.edgesForExtendedLayout = UIRectEdgeBottom; - this.extendedLayoutIncludesOpaqueBars = true; - } - - public viewWillLayoutSubviews(): void { - super.viewWillLayoutSubviews(); - const owner = this.owner.get(); - if (owner) { - updateConstraints(this, owner); - } - } - - public viewDidLayoutSubviews(): void { - super.viewDidLayoutSubviews(); - const owner = this.owner.get(); - if (owner) { - if (majorVersion >= 11) { - // Handle nested UILayoutViewController safe area application. - // Currently, UILayoutViewController can be nested only in a TabView. - // The TabView itself is handled by the OS, so we check the TabView's parent (usually a Page, but can be a Layout). - const tabViewItem = owner.parent; - const tabView = tabViewItem && tabViewItem.parent; - let parent = tabView && tabView.parent; - - // Handle Angular scenario where TabView is in a ProxyViewContainer - // It is possible to wrap components in ProxyViewContainers indefinitely - // Not using instanceof ProxyViewContainer to avoid circular dependency - // TODO: Try moving UILayoutViewController out of view module - while (parent && !parent.nativeViewProtected) { - parent = parent.parent; - } - - if (parent) { - const parentPageInsetsTop = parent.nativeViewProtected.safeAreaInsets.top; - const currentInsetsTop = this.view.safeAreaInsets.top; - const additionalInsetsTop = Math.max(parentPageInsetsTop - currentInsetsTop, 0); - - const parentPageInsetsBottom = parent.nativeViewProtected.safeAreaInsets.bottom; - const currentInsetsBottom = this.view.safeAreaInsets.bottom; - const additionalInsetsBottom = Math.max(parentPageInsetsBottom - currentInsetsBottom, 0); - - if (additionalInsetsTop > 0 || additionalInsetsBottom > 0) { - const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: additionalInsetsBottom, right: 0 }); - this.additionalSafeAreaInsets = additionalInsets; - } - } - } - - layoutView(this, owner); - } - } - - public viewWillAppear(animated: boolean): void { - super.viewWillAppear(animated); - const owner = this.owner.get(); - if (!owner) { - return; - } - - updateAutoAdjustScrollInsets(this, owner); - - if (!owner.parent) { - owner.callLoaded(); - } - } - - public viewDidDisappear(animated: boolean): void { - super.viewDidDisappear(animated); - const owner = this.owner.get(); - if (owner && !owner.parent) { - owner.callUnloaded(); - } - } - - // Mind implementation for other controllers - public traitCollectionDidChange(previousTraitCollection: UITraitCollection): void { - super.traitCollectionDidChange(previousTraitCollection); - - if (majorVersion >= 13) { - const owner = this.owner.get(); - if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { - owner.notify({ eventName: traitCollectionColorAppearanceChangedEvent, object: owner }); - } - } - } - } - - export class UIAdaptivePresentationControllerDelegateImp extends NSObject implements UIAdaptivePresentationControllerDelegate { - public static ObjCProtocols = [UIAdaptivePresentationControllerDelegate]; - - private owner: WeakRef; - private closedCallback: Function; - - public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIAdaptivePresentationControllerDelegateImp { - const instance = super.new(); - instance.owner = owner; - instance.closedCallback = whenClosedCallback; - - return instance; - } - - public presentationControllerDidDismiss(presentationController: UIPresentationController) { - const owner = this.owner.get(); - if (owner && typeof this.closedCallback === "function") { - this.closedCallback(); - } - } - } - - export class UIPopoverPresentationControllerDelegateImp extends NSObject implements UIPopoverPresentationControllerDelegate { - public static ObjCProtocols = [UIPopoverPresentationControllerDelegate]; - - private owner: WeakRef; - private closedCallback: Function; - - public static initWithOwnerAndCallback(owner: WeakRef, whenClosedCallback: Function): UIPopoverPresentationControllerDelegateImp { - const instance = super.new(); - instance.owner = owner; - instance.closedCallback = whenClosedCallback; - - return instance; - } - - public popoverPresentationControllerDidDismissPopover(popoverPresentationController: UIPopoverPresentationController) { - const owner = this.owner.get(); - if (owner && typeof this.closedCallback === "function") { - this.closedCallback(); - } - } - } -} diff --git a/nativescript-core/ui/frame/fragment.transitions.ios.ts b/nativescript-core/ui/frame/fragment.transitions.ios.ts index 7599a6093..6aa8f4c2c 100644 --- a/nativescript-core/ui/frame/fragment.transitions.ios.ts +++ b/nativescript-core/ui/frame/fragment.transitions.ios.ts @@ -69,7 +69,7 @@ class AnimatedTransitioning extends NSObject implements UIViewControllerAnimated } export function _createIOSAnimatedTransitioning(navigationTransition: NavigationTransition, nativeCurve: UIViewAnimationCurve, operation: UINavigationControllerOperation, fromVC: UIViewController, toVC: UIViewController): UIViewControllerAnimatedTransitioning { - const instance = navigationTransition.instance; + const instance = navigationTransition.instance; let transition: Transition; if (instance) { diff --git a/nativescript-core/ui/frame/frame-common.ts b/nativescript-core/ui/frame/frame-common.ts index ad5c67b8a..fbb4e67ac 100644 --- a/nativescript-core/ui/frame/frame-common.ts +++ b/nativescript-core/ui/frame/frame-common.ts @@ -1,23 +1,19 @@ -// Definitions. -import { Frame as FrameDefinition, NavigationEntry, BackstackEntry, NavigationTransition } from "."; -import { Page } from "../page"; - // Types. -import { getAncestor, viewMatchesModuleContext } from "../core/view/view-common"; +import { Frame as FrameDefinition } from "."; +import { BackstackEntry, NavigationContext, NavigationEntry, NavigationTransition, NavigationType } from "./frame-interfaces"; +import { Page } from "../page"; import { View, CustomLayoutView, isIOS, isAndroid, traceEnabled, traceWrite, traceCategories, Property, CSSType } from "../core/view"; + +// Requires. +import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack"; +import { getAncestor, viewMatchesModuleContext } from "../core/view/view-common"; import { Builder } from "../builder"; +import { sanitizeModuleName } from "../builder/module-name-sanitizer"; import { profile } from "../../profiling"; -import { frameStack, topmost as frameStackTopmost, _pushInFrameStack, _popFromFrameStack, _removeFromFrameStack } from "./frame-stack"; -import { sanitizeModuleName } from "../builder/module-name-sanitizer"; +export * from "./frame-interfaces"; export * from "../core/view"; -export enum NavigationType { - back, - forward, - replace -} - function buildEntryFromArgs(arg: any): NavigationEntry { let entry: NavigationEntry; if (typeof arg === "string") { @@ -35,13 +31,6 @@ function buildEntryFromArgs(arg: any): NavigationEntry { return entry; } -export interface NavigationContext { - entry: BackstackEntry; - // TODO: remove isBackNavigation for NativeScript 6.0 - isBackNavigation: boolean; - navigationType: NavigationType; -} - @CSSType("Frame") export class FrameBase extends CustomLayoutView implements FrameDefinition { public static androidOptionSelectedEvent = "optionSelected"; diff --git a/nativescript-core/ui/frame/frame-interfaces.ts b/nativescript-core/ui/frame/frame-interfaces.ts new file mode 100644 index 000000000..9c52509ad --- /dev/null +++ b/nativescript-core/ui/frame/frame-interfaces.ts @@ -0,0 +1,107 @@ +// Types +import { View } from "../core/view"; +import { Page } from "../page"; +import { Transition } from "../transition"; +import { Observable } from "../../data/observable"; + +export enum NavigationType { + back, + forward, + replace +} + +export interface TransitionState { + enterTransitionListener: any; + exitTransitionListener: any; + reenterTransitionListener: any; + returnTransitionListener: any; + transitionName: string; + entry: BackstackEntry; +} + +export interface ViewEntry { + moduleName?: string; + create?: () => View; +} + +export interface NavigationEntry extends ViewEntry { + context?: any; + bindingContext?: any; + animated?: boolean; + transition?: NavigationTransition; + transitioniOS?: NavigationTransition; + transitionAndroid?: NavigationTransition; + backstackVisible?: boolean; + clearHistory?: boolean; +} + +export interface NavigationContext { + entry: BackstackEntry; + // TODO: remove isBackNavigation for NativeScript 7.0 + isBackNavigation: boolean; + navigationType: NavigationType; +} + +export interface NavigationTransition { + name?: string; + instance?: Transition; + duration?: number; + curve?: any; +} + +export interface BackstackEntry { + entry: NavigationEntry; + resolvedPage: Page; + navDepth: number; + fragmentTag: string; + fragment?: any; + viewSavedState?: any; + frameId?: number; + recreated?: boolean; +} + +export interface AndroidFrame extends Observable { + rootViewGroup: any /* android.view.ViewGroup */; + activity: any /* androidx.appcompat.app.AppCompatActivity */; + currentActivity: any /* androidx.appcompat.app.AppCompatActivity */; + actionBar: any /* android.app.ActionBar */; + showActionBar: boolean; + fragmentForPage(entry: BackstackEntry): any; +} + +export interface AndroidActivityCallbacks { + getRootView(): View; + resetActivityContent(activity: any): void; + + onCreate(activity: any, savedInstanceState: any, intent: any, superFunc: Function): void; + onSaveInstanceState(activity: any, outState: any, superFunc: Function): void; + onStart(activity: any, superFunc: Function): void; + onStop(activity: any, superFunc: Function): void; + onPostResume(activity: any, superFunc: Function): void; + onDestroy(activity: any, superFunc: Function): void; + onBackPressed(activity: any, superFunc: Function): void; + onRequestPermissionsResult(activity: any, requestCode: number, permissions: Array, grantResults: Array, superFunc: Function): void; + onActivityResult(activity: any, requestCode: number, resultCode: number, data: any, superFunc: Function); + onNewIntent(activity: any, intent: any, superSetIntentFunc: Function, superFunc: Function): void; +} + +export interface AndroidFragmentCallbacks { + onHiddenChanged(fragment: any, hidden: boolean, superFunc: Function): void; + onCreateAnimator(fragment: any, transit: number, enter: boolean, nextAnim: number, superFunc: Function): any; + onCreate(fragment: any, savedInstanceState: any, superFunc: Function): void; + onCreateView(fragment: any, inflater: any, container: any, savedInstanceState: any, superFunc: Function): any; + onSaveInstanceState(fragment: any, outState: any, superFunc: Function): void; + onDestroyView(fragment: any, superFunc: Function): void; + onDestroy(fragment: any, superFunc: Function): void; + onPause(fragment: any, superFunc: Function): void; + onStop(fragment: any, superFunc: Function): void; + toStringOverride(fragment: any, superFunc: Function): string; +} + +/* tslint:disable */ +export interface iOSFrame { + /* tslint:enable */ + controller: any /* UINavigationController */; + navBarVisibility: "auto" | "never" | "always"; + _disableNavBarAnimation: boolean; +} diff --git a/nativescript-core/ui/frame/frame.android.ts b/nativescript-core/ui/frame/frame.android.ts index 8468ccf55..8a6946ad3 100644 --- a/nativescript-core/ui/frame/frame.android.ts +++ b/nativescript-core/ui/frame/frame.android.ts @@ -3,6 +3,7 @@ import { AndroidFrame as AndroidFrameDefinition, AndroidActivityCallbacks, AndroidFragmentCallbacks, BackstackEntry, NavigationTransition } from "."; +import { TransitionState } from "./frame-common"; import { Page } from "../page"; // Types. @@ -26,15 +27,6 @@ import { profile } from "../../profiling"; export * from "./frame-common"; -interface TransitionState { - enterTransitionListener: any; - exitTransitionListener: any; - reenterTransitionListener: any; - returnTransitionListener: any; - transitionName: string; - entry: BackstackEntry; -} - const ANDROID_PLATFORM = "android"; const INTENT_EXTRA = "com.tns.activity"; @@ -372,8 +364,10 @@ export class Frame extends FrameBase { return false; } + // HACK: This @profile decorator creates a circular dependency + // HACK: because the function parameter type is evaluated with 'typeof' @profile - public _navigateCore(newEntry: BackstackEntry) { + public _navigateCore(newEntry: any) { // should be (newEntry: BackstackEntry) super._navigateCore(newEntry); // set frameId here so that we could use it in fragment.transitions @@ -1285,12 +1279,11 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { savedInstanceState: android.os.Bundle, fireLaunchEvent: boolean ): void { - const shouldCreateRootFrame = application._shouldCreateRootFrame(); let rootView = this._rootView; if (traceEnabled()) { traceWrite( - `Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: ${shouldCreateRootFrame} fireLaunchEvent: ${fireLaunchEvent}`, + `Frame.setActivityContent rootView: ${rootView} shouldCreateRootFrame: false fireLaunchEvent: ${fireLaunchEvent}`, traceCategories.NativeLifecycle ); } @@ -1311,36 +1304,7 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { throw new Error("Main entry is missing. App cannot be started. Verify app bootstrap."); } - if (shouldCreateRootFrame) { - const extras = intent.getExtras(); - let frameId = -1; - - // We have extras when we call - new Frame().navigate(); - // savedInstanceState is used when activity is recreated. - // NOTE: On API 23+ we get extras on first run. - // Check changed - first try to get frameId from Extras if not from saveInstanceState. - if (extras) { - frameId = extras.getInt(INTENT_EXTRA, -1); - } - - if (savedInstanceState && frameId < 0) { - frameId = savedInstanceState.getInt(INTENT_EXTRA, -1); - } - - if (!rootView) { - // If we have frameId from extras - we are starting a new activity from navigation (e.g. new Frame().navigate())) - // Then we check if we have frameId from savedInstanceState - this happens when Activity is destroyed but app was not (e.g. suspend) - rootView = getFrameByNumberId(frameId) || new Frame(); - } - - if (rootView instanceof Frame) { - rootView.navigate(mainEntry); - } else { - throw new Error("A Frame must be used to navigate to a Page."); - } - } else { - rootView = Builder.createViewFromEntry(mainEntry); - } + rootView = Builder.createViewFromEntry(mainEntry); } this._rootView = rootView; @@ -1356,14 +1320,8 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { rootViewCssClasses.forEach(c => this._rootView.cssClasses.add(c)); } - // Initialize native visual tree; - if (shouldCreateRootFrame) { - // Don't setup as styleScopeHost - rootView._setupUI(activity); - } else { - // setup view as styleScopeHost - rootView._setupAsRootView(activity); - } + // setup view as styleScopeHost + rootView._setupAsRootView(activity); activity.setContentView(rootView.nativeViewProtected, new org.nativescript.widgets.CommonLayoutParams()); } diff --git a/nativescript-core/ui/frame/frame.ios.ts b/nativescript-core/ui/frame/frame.ios.ts index 145754bfb..f31b12b93 100644 --- a/nativescript-core/ui/frame/frame.ios.ts +++ b/nativescript-core/ui/frame/frame.ios.ts @@ -1,23 +1,22 @@ -// Definitions. -import { - iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition -} from "."; -import { ios as iosView } from "../core/view"; +//Types +import { iOSFrame as iOSFrameDefinition, BackstackEntry, NavigationTransition } from "."; +import { FrameBase, NavigationType } from "./frame-common"; import { Page } from "../page"; -import { profile } from "../../profiling"; +import { View } from "../core/view"; -//Types. -import { - FrameBase, View, isCategorySet, layout, - NavigationType, traceCategories, traceEnabled, traceWrite -} from "./frame-common"; +// Requires import { _createIOSAnimatedTransitioning } from "./fragment.transitions"; - -import * as utils from "../../utils/utils"; +import { ios as iosViewHelper } from "../core/view/view-helper"; +import { profile } from "../../profiling"; +import { ios as iosUtils, layout } from "../../utils/utils"; +import { + isCategorySet, isEnabled as traceEnabled, + categories as traceCategories, write as traceWrite +} from "../../trace"; export * from "./frame-common"; -const majorVersion = utils.ios.MajorVersion; +const majorVersion = iosUtils.MajorVersion; const ENTRY = "_entry"; const DELEGATE = "_delegate"; @@ -64,8 +63,10 @@ export class Frame extends FrameBase { } } + // !!! THIS PROFILE DECORATOR CREATES A CIRCULAR DEPENDENCY + // !!! BECAUSE THE PARAMETER TYPE IS EVALUATED WITH TYPEOF @profile - public _navigateCore(backstackEntry: BackstackEntry) { + public _navigateCore(backstackEntry: any) { super._navigateCore(backstackEntry); let viewController: UIViewController = backstackEntry.resolvedPage.ios; @@ -541,7 +542,7 @@ class UINavigationControllerImpl extends UINavigationController { if (majorVersion >= 13) { const owner = this._owner.get(); if (owner && this.traitCollection.hasDifferentColorAppearanceComparedToTraitCollection(previousTraitCollection)) { - owner.notify({ eventName: iosView.traitCollectionColorAppearanceChangedEvent, object: owner }); + owner.notify({ eventName: iosViewHelper.traitCollectionColorAppearanceChangedEvent, object: owner }); } } } diff --git a/nativescript-core/ui/styling/font-common.ts b/nativescript-core/ui/styling/font-common.ts index 3d910dc90..fca83e612 100644 --- a/nativescript-core/ui/styling/font-common.ts +++ b/nativescript-core/ui/styling/font-common.ts @@ -1,6 +1,9 @@ -import { Font as FontDefinition, ParsedFont } from "./font"; +import { Font as FontDefinition } from "./font"; +import { ParsedFont } from "./font-interfaces"; import { makeValidator, makeParser } from "../core/properties"; +export * from "./font-interfaces"; + export abstract class Font implements FontDefinition { public static default = undefined; diff --git a/nativescript-core/ui/styling/font-interfaces.ts b/nativescript-core/ui/styling/font-interfaces.ts new file mode 100644 index 000000000..fab26d0e3 --- /dev/null +++ b/nativescript-core/ui/styling/font-interfaces.ts @@ -0,0 +1,12 @@ +export type FontStyle = "normal" | "italic"; + +export type FontWeight = "100" | "200" | "300" | "normal" | "400" | "500" | "600" | "bold" | "700" | "800" | "900"; + +export interface ParsedFont { + fontStyle?: FontStyle; + fontVariant?: string; + fontWeight?: FontWeight; + lineHeight?: string; + fontSize?: string; + fontFamily?: string; +} diff --git a/nativescript-core/ui/styling/font.d.ts b/nativescript-core/ui/styling/font.d.ts index c17925d4b..25753d884 100644 --- a/nativescript-core/ui/styling/font.d.ts +++ b/nativescript-core/ui/styling/font.d.ts @@ -49,7 +49,7 @@ export namespace FontWeight { export function parse(value: string): FontWeight; } -interface ParsedFont { +export interface ParsedFont { fontStyle?: FontStyle; fontVariant?: string; fontWeight?: FontWeight, diff --git a/nativescript-core/ui/styling/style-scope.ts b/nativescript-core/ui/styling/style-scope.ts index 6105cff89..ed585227d 100644 --- a/nativescript-core/ui/styling/style-scope.ts +++ b/nativescript-core/ui/styling/style-scope.ts @@ -28,7 +28,7 @@ import { messageType as traceMessageType, } from "../../trace"; import { File, knownFolders, path } from "../../file-system"; -import * as applicationCommon from "../../application/application-common"; +import * as application from "../../application"; import { profile } from "../../profiling"; import * as kam from "../animation/keyframe-animation"; @@ -330,7 +330,7 @@ export function addTaggedAdditionalCSS(cssText: string, tag?: string | Number): return changed; } -const onCssChanged = profile("\"style-scope\".onCssChanged", (args: applicationCommon.CssChangedEventData) => { +const onCssChanged = profile("\"style-scope\".onCssChanged", (args: application.CssChangedEventData) => { if (args.cssText) { const parsed = CSSSource.fromSource(args.cssText, applicationKeyframes, args.cssFile).selectors; if (parsed) { @@ -342,8 +342,8 @@ const onCssChanged = profile("\"style-scope\".onCssChanged", (args: applicationC } }); -function onLiveSync(args: applicationCommon.CssChangedEventData): void { - loadCss(applicationCommon.getCssFileName()); +function onLiveSync(args: application.CssChangedEventData): void { + loadCss(application.getCssFileName()); } const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => { @@ -363,23 +363,23 @@ const loadCss = profile(`"style-scope".loadCss`, (cssModule: string) => { } }); -applicationCommon.on("cssChanged", onCssChanged); -applicationCommon.on("livesync", onLiveSync); +application.on("cssChanged", onCssChanged); +application.on("livesync", onLiveSync); // Call to this method is injected in the application in: // - no-snapshot - code injected in app.ts by [bundle-config-loader](https://github.com/NativeScript/nativescript-dev-webpack/blob/9b1e34d8ef838006c9b575285c42d2304f5f02b5/bundle-config-loader.ts#L85-L92) // - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56) // Having the app.css loaded in snapshot provides significant boost in startup (when using the ns-theme ~150 ms). However, because app.css is resolved at build-time, // when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css -export const loadAppCSS = profile("\"style-scope\".loadAppCSS", (args: applicationCommon.LoadAppCSSEventData) => { +export const loadAppCSS = profile("\"style-scope\".loadAppCSS", (args: application.LoadAppCSSEventData) => { loadCss(args.cssFile); - applicationCommon.off("loadAppCss", loadAppCSS); + application.off("loadAppCss", loadAppCSS); }); -if (applicationCommon.hasLaunched()) { - loadAppCSS({ eventName: "loadAppCss", object: applicationCommon, cssFile: applicationCommon.getCssFileName() }); +if (application.hasLaunched()) { + loadAppCSS({ eventName: "loadAppCss", object: application, cssFile: application.getCssFileName() }); } else { - applicationCommon.on("loadAppCss", loadAppCSS); + application.on("loadAppCss", loadAppCSS); } export class CssState { @@ -790,8 +790,10 @@ export class StyleScope { } } + // HACK: This @profile decorator creates a circular dependency + // HACK: because the function parameter type is evaluated with 'typeof' @profile - public matchSelectors(view: ViewBase): SelectorsMatch { + public matchSelectors(view: any): SelectorsMatch { // should be (view: ViewBase): SelectorsMatch this.ensureSelectors(); return this._selectors.query(view); diff --git a/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts b/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts index 09ea676e7..10d846c7e 100644 --- a/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts +++ b/nativescript-core/ui/tab-navigation-base/tab-strip-item/tab-strip-item.ts @@ -213,7 +213,7 @@ export class TabStripItem extends View implements TabStripItemDefinition, AddChi const parent = this.parent; const tabStripParent = parent && parent.parent; if (this._index === tabStripParent.selectedIndex && - !(isIOS && tabStripParent instanceof Tabs)) { + !(isIOS && tabStripParent.cssType.toLowerCase() === "tabs")) { this._goToVisualState("highlighted"); } } else { diff --git a/nativescript-core/ui/text-base/text-base-common.ts b/nativescript-core/ui/text-base/text-base-common.ts index 3115711e7..a5da6f3c3 100644 --- a/nativescript-core/ui/text-base/text-base-common.ts +++ b/nativescript-core/ui/text-base/text-base-common.ts @@ -1,13 +1,19 @@ -// Definitions. -import { TextBase as TextBaseDefinition, TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "."; -import { FontStyle, FontWeight } from "../styling/font"; -import { PropertyChangeData } from "../../data/observable"; +// Types +import { TextBase as TextBaseDefinition } from "."; +import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from "./text-base-interfaces"; +import { Length, ViewBase } from "../core/view"; +import { FontStyle, FontWeight } from "../styling/font-interfaces"; +import { PropertyChangeData } from "../../data/observable/observable-interfaces"; -// Types. -import { View, ViewBase, Property, CssProperty, InheritedCssProperty, Style, isAndroid, isIOS, Observable, makeValidator, makeParser, Length } from "../core/view"; +// Requires. import { FormattedString, Span } from "./formatted-string"; +import { + View, Property, CssProperty, InheritedCssProperty, Style, isAndroid, isIOS, Observable, + makeValidator, makeParser +} from "../core/view"; export { FormattedString, Span }; +export * from "./text-base-interfaces"; export * from "../core/view"; const CHILD_SPAN = "Span"; diff --git a/nativescript-core/ui/text-base/text-base-interfaces.ts b/nativescript-core/ui/text-base/text-base-interfaces.ts new file mode 100644 index 000000000..2115210ec --- /dev/null +++ b/nativescript-core/ui/text-base/text-base-interfaces.ts @@ -0,0 +1,11 @@ +// Types +import { TextBase } from "./text-base"; + +export interface TextTransformation { + new(owner: TextBase): any /* android.text.method.TransformationMethod */; +} + +export type WhiteSpace = "initial" | "normal" | "nowrap"; +export type TextAlignment = "initial" | "left" | "center" | "right"; +export type TextTransform = "initial" | "none" | "capitalize" | "uppercase" | "lowercase"; +export type TextDecoration = "none" | "underline" | "line-through" | "underline line-through"; diff --git a/nativescript-core/ui/text-base/text-base.android.ts b/nativescript-core/ui/text-base/text-base.android.ts index 258a1ab4a..d7cf1bfa7 100644 --- a/nativescript-core/ui/text-base/text-base.android.ts +++ b/nativescript-core/ui/text-base/text-base.android.ts @@ -1,4 +1,7 @@ -import { TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base"; +// Types +import { TextTransformation, TextDecoration, TextAlignment, TextTransform, WhiteSpace } from "./text-base-common"; + +// Requires import { Font } from "../styling/font"; import { backgroundColorProperty } from "../styling/style-properties"; import { @@ -11,10 +14,6 @@ import { isString } from "../../utils/types"; export * from "./text-base-common"; -interface TextTransformation { - new(owner: TextBase): android.text.method.TransformationMethod; -} - let TextTransformation: TextTransformation; function initializeTextTransformation(): void { diff --git a/nativescript-core/ui/text-base/text-base.ios.ts b/nativescript-core/ui/text-base/text-base.ios.ts index 4665757c0..a96fcaed0 100644 --- a/nativescript-core/ui/text-base/text-base.ios.ts +++ b/nativescript-core/ui/text-base/text-base.ios.ts @@ -1,16 +1,18 @@ -import { TextDecoration, TextAlignment, TextTransform } from "./text-base"; +// Types +import { TextDecoration, TextAlignment, TextTransform } from "./text-base-common"; + +// Requires import { Font } from "../styling/font"; import { TextBaseCommon, textProperty, formattedTextProperty, textAlignmentProperty, textDecorationProperty, textTransformProperty, letterSpacingProperty, colorProperty, fontInternalProperty, lineHeightProperty, FormattedString, Span, Color, isBold, resetSymbol } from "./text-base-common"; - -export * from "./text-base-common"; - import { isString } from "../../utils/types"; import { ios } from "../../utils/utils"; +export * from "./text-base-common"; + const majorVersion = ios.MajorVersion; export class TextBase extends TextBaseCommon { diff --git a/nativescript-core/ui/web-view/web-view-common.ts b/nativescript-core/ui/web-view/web-view-common.ts index 36acc9da0..bced3983b 100644 --- a/nativescript-core/ui/web-view/web-view-common.ts +++ b/nativescript-core/ui/web-view/web-view-common.ts @@ -1,8 +1,10 @@ -import { WebView as WebViewDefinition, LoadEventData, NavigationType } from "."; +import { WebView as WebViewDefinition } from "."; +import { LoadEventData, NavigationType } from "./web-view-interfaces"; import { ContainerView, Property, EventData, CSSType } from "../core/view"; import { File, knownFolders, path } from "../../file-system"; -export { File, knownFolders, path, NavigationType }; +export { File, knownFolders, path }; +export * from "./web-view-interfaces"; export * from "../core/view"; export const srcProperty = new Property({ name: "src" }); @@ -87,6 +89,10 @@ export abstract class WebViewBase extends ContainerView implements WebViewDefini } } +// HACK: Use an interface with the same name, so that the class above fulfills the 'implements' requirement +// HACK: We use the 'implements' to verify the class above is the same as the one declared in the d.ts +// HACK: We declare all these 'on' statements, so that they can appear in the API reference +// HACK: Do we need this? Is it useful? There are static fields to the WebViewBase class for the event names. export interface WebViewBase { on(eventNames: string, callback: (data: EventData) => void, thisArg?: any); on(event: "loadFinished", callback: (args: LoadEventData) => void, thisArg?: any); diff --git a/nativescript-core/ui/web-view/web-view-interfaces.ts b/nativescript-core/ui/web-view/web-view-interfaces.ts new file mode 100644 index 000000000..a70343458 --- /dev/null +++ b/nativescript-core/ui/web-view/web-view-interfaces.ts @@ -0,0 +1,14 @@ +import { WebView } from "."; +import { EventData } from "../core/view"; + +export type NavigationType = "linkClicked" | "formSubmitted" | "backForward" | "reload" | "formResubmitted" | "other" | undefined; + +export interface LoadEventData extends EventData { + url: string; + navigationType: NavigationType; + error: string; +} + +export interface WebViewClient { + new(owner: WebView): any /* android.webkit.WebViewClient */; +} diff --git a/nativescript-core/ui/web-view/web-view.android.ts b/nativescript-core/ui/web-view/web-view.android.ts index 497d0e14f..3b3aacdec 100644 --- a/nativescript-core/ui/web-view/web-view.android.ts +++ b/nativescript-core/ui/web-view/web-view.android.ts @@ -1,11 +1,7 @@ -import { WebViewBase, knownFolders, traceEnabled, traceWrite, traceCategories } from "./web-view-common"; +import { WebViewBase, knownFolders, traceEnabled, traceWrite, traceCategories, WebViewClient } from "./web-view-common"; export * from "./web-view-common"; -interface WebViewClient { - new(owner: WebView): android.webkit.WebViewClient; -} - let WebViewClient: WebViewClient; function initializeWebViewClient(): void { diff --git a/nativescript-core/ui/web-view/web-view.ios.ts b/nativescript-core/ui/web-view/web-view.ios.ts index 71b39ab58..534cd7497 100644 --- a/nativescript-core/ui/web-view/web-view.ios.ts +++ b/nativescript-core/ui/web-view/web-view.ios.ts @@ -1,4 +1,5 @@ -import { WebViewBase, knownFolders, traceWrite, traceEnabled, traceCategories, NavigationType } from "./web-view-common"; +import { NavigationType } from "."; +import { WebViewBase, knownFolders, traceWrite, traceEnabled, traceCategories } from "./web-view-common"; import { profile } from "../../profiling"; export * from "./web-view-common"; diff --git a/nativescript-core/utils/layout-helper/layout-helper-common.ts b/nativescript-core/utils/layout-helper/layout-helper-common.ts new file mode 100644 index 000000000..eea75d82c --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper-common.ts @@ -0,0 +1,63 @@ +// cache the MeasureSpec constants here, to prevent extensive marshaling calls to and from Java +// TODO: While this boosts the performance it is error-prone in case Google changes these constants +export const MODE_SHIFT = 30; +export const MODE_MASK = 0x3 << MODE_SHIFT; + +export const UNSPECIFIED = 0 << MODE_SHIFT; +export const EXACTLY = 1 << MODE_SHIFT; +export const AT_MOST = 2 << MODE_SHIFT; + +export const MEASURED_HEIGHT_STATE_SHIFT = 0x00000010; /* 16 */ +export const MEASURED_STATE_TOO_SMALL = 0x01000000; +export const MEASURED_STATE_MASK = 0xff000000; +export const MEASURED_SIZE_MASK = 0x00ffffff; + +export function getMode(mode: number): string { + switch (mode) { + case EXACTLY: + return "Exact"; + case AT_MOST: + return "AtMost"; + default: + return "Unspecified"; + } +} + +export function getMeasureSpecMode(spec: number): number { + return (spec & MODE_MASK); +} + +export function getMeasureSpecSize(spec: number): number { + return (spec & ~MODE_MASK); +} + +export function measureSpecToString(measureSpec: number): string { + const mode = getMeasureSpecMode(measureSpec); + const size = getMeasureSpecSize(measureSpec); + + let text = "MeasureSpec: "; + if (mode === UNSPECIFIED) { + text += "UNSPECIFIED "; + } else if (mode === EXACTLY) { + text += "EXACTLY "; + } else if (mode === AT_MOST) { + text += "AT_MOST "; + } + + text += size; + + return text; +} + +export function round(value: number): number { + const res = Math.floor(value + 0.5); + if (res !== 0) { + return res; + } else if (value === 0) { + return 0; + } else if (value > 0) { + return 1; + } + + return -1; +} diff --git a/nativescript-core/utils/layout-helper/layout-helper.android.ts b/nativescript-core/utils/layout-helper/layout-helper.android.ts new file mode 100644 index 000000000..c5135e94b --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.android.ts @@ -0,0 +1,49 @@ +import { MODE_MASK } from "./layout-helper-common"; +import { ad } from "../native-helper"; + +export * from "./layout-helper-common"; + +let density: number; + +let sdkVersion: number; +let useOldMeasureSpec = false; + +export function makeMeasureSpec(size: number, mode: number): number { + if (sdkVersion === undefined) { + // check whether the old layout is needed + sdkVersion = ad.getApplicationContext().getApplicationInfo().targetSdkVersion; + useOldMeasureSpec = sdkVersion <= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; + } + + if (useOldMeasureSpec) { + return size + mode; + } + + return (size & ~MODE_MASK) | (mode & MODE_MASK); +} + +export function getDisplayDensity(): number { + if (density === undefined) { + density = ad.getResources().getDisplayMetrics().density; + } + + return density; +} + +export function toDevicePixels(value: number): number { + return value * getDisplayDensity(); +} + +export function toDeviceIndependentPixels(value: number): number { + return value / getDisplayDensity(); +} + +export function measureNativeView(nativeView: any /* android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { + const view = nativeView; + view.measure(makeMeasureSpec(width, widthMode), makeMeasureSpec(height, heightMode)); + + return { + width: view.getMeasuredWidth(), + height: view.getMeasuredHeight() + }; +} diff --git a/nativescript-core/utils/layout-helper/layout-helper.d.ts b/nativescript-core/utils/layout-helper/layout-helper.d.ts new file mode 100644 index 000000000..5ff316060 --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.d.ts @@ -0,0 +1,77 @@ +import { dip, px } from "../../ui/core/view"; + +/** + * Bits that provide the actual measured size. + */ +export const MEASURED_HEIGHT_STATE_SHIFT: number; +export const MEASURED_SIZE_MASK: number; +export const MEASURED_STATE_MASK: number; +export const MEASURED_STATE_TOO_SMALL: number; +export const UNSPECIFIED: number; +export const EXACTLY: number; +export const AT_MOST: number; + +/** + * Gets layout mode from a given specification as string. + * @param mode - The measure specification mode. + */ +export function getMode(mode: number): string; + +/** + * Gets measure specification mode from a given specification. + * @param spec - The measure specification. + */ +export function getMeasureSpecMode(spec: number): number; + +/** + * Gets measure specification size from a given specification. + * @param spec - The measure specification. + */ +export function getMeasureSpecSize(spec: number): number; + +/** + * Creates measure specification size from size and mode. + * @param size - The size component of measure specification. + * @param mode - The mode component of measure specification. + */ +export function makeMeasureSpec(px: number, mode: number): number; + +/** + * Gets display density for the current device. + */ +export function getDisplayDensity(): number; + +/** + * Convert device independent pixels to device pixels - dip to px. + * @param value - The pixel to convert. + */ +export function toDevicePixels(value: dip): px; + +/** + * Convert device pixels to device independent pixels - px to dip. + * @param value - The pixel to convert. + */ +export function toDeviceIndependentPixels(value: px): dip; + +/** + * Rounds value used in layout. + * @param px to round. + */ +export function round(px: px): px; + +/** + * Converts device pixels to device independent pixes and measure the nativeView. + * Returns the desired size of the nativeView in device pixels. + * @param nativeView the nativeView to measure (UIView or android.view.View) + * @param width the available width + * @param widthMode width mode - UNSPECIFIED, EXACTLY or AT_MOST + * @param height the available hegiht + * @param heightMode height mode - UNSPECIFIED, EXACTLY or AT_MOST + */ +export function measureNativeView(nativeView: any /* UIView or android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number }; + +/** + * Prints user friendly version of the measureSpec. + * @param measureSpec the spec to print + */ +export function measureSpecToString(measureSpec: number): string; diff --git a/nativescript-core/utils/layout-helper/layout-helper.ios.ts b/nativescript-core/utils/layout-helper/layout-helper.ios.ts new file mode 100644 index 000000000..d3450bbfb --- /dev/null +++ b/nativescript-core/utils/layout-helper/layout-helper.ios.ts @@ -0,0 +1,36 @@ +import { round, MODE_MASK } from "./layout-helper-common"; + +export * from "./layout-helper-common"; + +let mainScreenScale; + +export function makeMeasureSpec(size: number, mode: number): number { + return (Math.round(Math.max(0, size)) & ~MODE_MASK) | (mode & MODE_MASK); +} + +export function getDisplayDensity(): number { + return mainScreenScale; +} + +export function toDevicePixels(value: number): number { + return value * mainScreenScale; +} + +export function toDeviceIndependentPixels(value: number): number { + return value / mainScreenScale; +} + +export function measureNativeView(nativeView: any /* UIView */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { + const view = nativeView; + const nativeSize = view.sizeThatFits({ + width: widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(width), + height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height) + }); + + nativeSize.width = round(toDevicePixels(nativeSize.width)); + nativeSize.height = round(toDevicePixels(nativeSize.height)); + + return nativeSize; +} + +mainScreenScale = UIScreen.mainScreen.scale; diff --git a/nativescript-core/utils/layout-helper/package.json b/nativescript-core/utils/layout-helper/package.json new file mode 100644 index 000000000..4d020b6a2 --- /dev/null +++ b/nativescript-core/utils/layout-helper/package.json @@ -0,0 +1,6 @@ +{ + "name": "layout-helper", + "main": "layout-helper", + "types": "layout-helper.d.ts", + "nativescript": {} +} \ No newline at end of file diff --git a/nativescript-core/utils/native-helper.android.ts b/nativescript-core/utils/native-helper.android.ts new file mode 100644 index 000000000..6d848179c --- /dev/null +++ b/nativescript-core/utils/native-helper.android.ts @@ -0,0 +1,156 @@ +import { getNativeApplication, android as androidApp } from "../application"; +import { + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../trace"; + +// We are using "ad" here to avoid namespace collision with the global android object +export module ad { + + let application: android.app.Application; + let applicationContext: android.content.Context; + let contextResources: android.content.res.Resources; + let packageName: string; + export function getApplicationContext() { + if (!applicationContext) { + applicationContext = getApplication().getApplicationContext(); + } + + return applicationContext; + } + export function getApplication() { + if (!application) { + application = (getNativeApplication()); + } + + return application; + } + export function getResources() { + if (!contextResources) { + contextResources = getApplication().getResources(); + } + + return contextResources; + } + function getPackageName() { + if (!packageName) { + packageName = getApplicationContext().getPackageName(); + } + + return packageName; + } + + let inputMethodManager: android.view.inputmethod.InputMethodManager; + export function getInputMethodManager(): android.view.inputmethod.InputMethodManager { + if (!inputMethodManager) { + inputMethodManager = getApplicationContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); + } + + return inputMethodManager; + } + + export function showSoftInput(nativeView: android.view.View): void { + const inputManager = getInputMethodManager(); + if (inputManager && nativeView instanceof android.view.View) { + inputManager.showSoftInput(nativeView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); + } + } + + export function dismissSoftInput(nativeView?: android.view.View): void { + const inputManager = getInputMethodManager(); + let windowToken: android.os.IBinder; + + if (nativeView instanceof android.view.View) { + windowToken = nativeView.getWindowToken(); + } else if (androidApp.foregroundActivity instanceof androidx.appcompat.app.AppCompatActivity) { + const decorView = androidApp.foregroundActivity.getWindow().getDecorView(); + windowToken = decorView ? decorView.getWindowToken() : null; + } + + if (inputManager && windowToken) { + inputManager.hideSoftInputFromWindow(windowToken, 0); + } + } + + export module collections { + export function stringArrayToStringSet(str: string[]): java.util.HashSet { + const hashSet = new java.util.HashSet(); + if (str !== undefined) { + for (let element in str) { + hashSet.add("" + str[element]); + } + } + + return hashSet; + } + + export function stringSetToStringArray(stringSet: any): string[] { + const arr = []; + if (stringSet !== undefined) { + const it = stringSet.iterator(); + while (it.hasNext()) { + const element = "" + it.next(); + arr.push(element); + } + } + + return arr; + } + } + + export module resources { + let attr; + const attrCache = new Map(); + + export function getDrawableId(name) { + return getId(":drawable/" + name); + } + + export function getStringId(name) { + return getId(":string/" + name); + } + + export function getId(name: string): number { + const resources = getResources(); + const packageName = getPackageName(); + const uri = packageName + name; + + return resources.getIdentifier(uri, null, null); + } + export function getPalleteColor(name: string, context: android.content.Context): number { + return getPaletteColor(name, context); + } + export function getPaletteColor(name: string, context: android.content.Context): number { + if (attrCache.has(name)) { + return attrCache.get(name); + } + + let result = 0; + try { + if (!attr) { + attr = java.lang.Class.forName("androidx.appcompat.R$attr"); + } + + let colorID = 0; + let field = attr.getField(name); + if (field) { + colorID = field.getInt(null); + } + + if (colorID) { + let typedValue = new android.util.TypedValue(); + context.getTheme().resolveAttribute(colorID, typedValue, true); + result = typedValue.data; + } + } + catch (ex) { + traceWrite("Cannot get pallete color: " + name, traceCategories.Error, traceMessageType.error); + } + + attrCache.set(name, result); + + return result; + } + } +} \ No newline at end of file diff --git a/nativescript-core/utils/native-helper.d.ts b/nativescript-core/utils/native-helper.d.ts new file mode 100644 index 000000000..3dc4ff457 --- /dev/null +++ b/nativescript-core/utils/native-helper.d.ts @@ -0,0 +1,159 @@ +/** + * Module with android specific utilities. + */ +export module ad { + /** + * Gets the native Android application instance. + */ + export function getApplication(): any /* android.app.Application */; + + /** + * Gets the native Android application resources. + */ + export function getResources(): any /* android.content.res.Resources */; + + /** + * Gets the Android application context. + */ + export function getApplicationContext(): any /* android.content.Context */; + + /** + * Gets the native Android input method manager. + */ + export function getInputMethodManager(): any /* android.view.inputmethod.InputMethodManager */; + + /** + * Hides the soft input method, usually a soft keyboard. + */ + export function dismissSoftInput(nativeView?: any /* android.view.View */): void; + + /** + * Shows the soft input method, usually a soft keyboard. + */ + export function showSoftInput(nativeView: any /* android.view.View */): void; + + /** + * Utility module dealing with some android collections. + */ + module collections { + /** + * Converts string array into a String [hash set](http://developer.android.com/reference/java/util/HashSet.html). + * @param str - An array of strings to convert. + */ + export function stringArrayToStringSet(str: string[]): any; + + /** + * Converts string hash set into array of strings. + * @param stringSet - A string hash set to convert. + */ + export function stringSetToStringArray(stringSet: any): string[]; + } + + /** + * Utility module related to android resources. + */ + export module resources { + /** + * Gets the drawable id from a given name. + * @param name - Name of the resource. + */ + export function getDrawableId(name); + + /** + * Gets the string id from a given name. + * @param name - Name of the resource. + */ + export function getStringId(name) + + /** + * Gets the id from a given name. + * @param name - Name of the resource. + */ + export function getId(name: string): number; + + /** + * [Obsolete - please use getPaletteColor] Gets a color from current theme. + * @param name - Name of the color + */ + export function getPalleteColor(); + + /** + * Gets a color from the current theme. + * @param name - Name of the color resource. + */ + export function getPaletteColor(name: string, context: any /* android.content.Context */): number; + } +} +/** + * Module with ios specific utilities. + */ +export module ios { + + // Common properties between UILabel, UITextView and UITextField + export interface TextUIView { + font: any; + textAlignment: number; + textColor: any; + text: string; + attributedText: any; + lineBreakMode: number; + numberOfLines: number; + } + + /** + * Utility module dealing with some iOS collections. + */ + module collections { + /** + * Converts JavaScript array to [NSArray](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/). + * @param str - JavaScript string array to convert. + */ + export function jsArrayToNSArray(str: string[]): any; + + /** + * Converts NSArray to JavaScript array. + * @param a - NSArray to convert. + */ + export function nsArrayToJSArray(a: any): string[]; + } + + /** + * @deprecated use application.orientation instead + * + * Gets an information about if current mode is Landscape. + */ + export function isLandscape(): boolean; + + /** + * Gets the iOS device major version (for 8.1 will return 8). + */ + export const MajorVersion: number; + + /** + * Opens file with associated application. + * @param filePath The file path. + */ + export function openFile(filePath: string): boolean; + + /** + * Joins an array of file paths. + * @param paths An array of paths. + * Returns the joined path. + */ + export function joinPaths(...paths: string[]): string; + + /** + * Gets the root folder for the current application. This Folder is private for the application and not accessible from Users/External apps. + * iOS - this folder is read-only and contains the app and all its resources. + */ + export function getCurrentAppPath(): string; + + /** + * Gets the currently visible(topmost) UIViewController. + * @param rootViewController The root UIViewController instance to start searching from (normally window.rootViewController). + * Returns the visible UIViewController. + */ + export function getVisibleViewController(rootViewController: any/* UIViewController*/): any/* UIViewController*/; + + export class UIDocumentInteractionControllerDelegateImpl {} +} diff --git a/nativescript-core/utils/native-helper.ios.ts b/nativescript-core/utils/native-helper.ios.ts new file mode 100644 index 000000000..219f7c3db --- /dev/null +++ b/nativescript-core/utils/native-helper.ios.ts @@ -0,0 +1,138 @@ +import { + messageType as traceMessageType, + categories as traceCategories, + write as traceWrite +} from "../trace"; + +function isOrientationLandscape(orientation: number) { + return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || + orientation === UIDeviceOrientation.LandscapeRight /* 4 */; +} + +function openFileAtRootModule(filePath: string): boolean { + try { + const appPath = ios.getCurrentAppPath(); + const path = filePath.replace("~", appPath); + + const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path)); + controller.delegate = new ios.UIDocumentInteractionControllerDelegateImpl(); + + return controller.presentPreviewAnimated(true); + } + catch (e) { + traceWrite("Error in openFile", traceCategories.Error, traceMessageType.error); + } + + return false; +} + +export module ios { + // TODO: remove for NativeScript 7.0 + export function getter(_this: any, property: T | { (): T }): T { + console.log("utils.ios.getter() is deprecated; use the respective native property instead"); + if (typeof property === "function") { + return (<{ (): T }>property).call(_this); + } else { + return property; + } + } + + export module collections { + export function jsArrayToNSArray(str: string[]): NSArray { + return NSArray.arrayWithArray(str); + } + + export function nsArrayToJSArray(a: NSArray): Array { + const arr = []; + if (a !== undefined) { + let count = a.count; + for (let i = 0; i < count; i++) { + arr.push(a.objectAtIndex(i)); + } + } + + return arr; + } + } + + export function isLandscape(): boolean { + console.log("utils.ios.isLandscape() is deprecated; use application.orientation instead"); + + const deviceOrientation = UIDevice.currentDevice.orientation; + const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; + + const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation); + const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation); + + return isDeviceOrientationLandscape || isStatusBarOrientationLandscape; + } + + export const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue; + + export function openFile(filePath: string): boolean { + console.log("utils.ios.openFile() is deprecated; use utils.openFile() instead"); + + return openFileAtRootModule(filePath); + } + + export function getCurrentAppPath(): string { + const currentDir = __dirname; + const tnsModulesIndex = currentDir.indexOf("/tns_modules"); + + // Module not hosted in ~/tns_modules when bundled. Use current dir. + let appPath = currentDir; + if (tnsModulesIndex !== -1) { + // Strip part after tns_modules to obtain app root + appPath = currentDir.substring(0, tnsModulesIndex); + } + + return appPath; + } + + export function joinPaths(...paths: string[]): string { + if (!paths || paths.length === 0) { + return ""; + } + + return NSString.stringWithString(NSString.pathWithComponents(paths)).stringByStandardizingPath; + } + + export function getVisibleViewController(rootViewController: UIViewController): UIViewController { + if (rootViewController.presentedViewController) { + return getVisibleViewController(rootViewController.presentedViewController); + } + + if (rootViewController.isKindOfClass(UINavigationController.class())) { + return getVisibleViewController((rootViewController).visibleViewController); + } + + if (rootViewController.isKindOfClass(UITabBarController.class())) { + return getVisibleViewController(rootViewController); + } + + return rootViewController; + + } + + export class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { + public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; + + public getViewController(): UIViewController { + const app = UIApplication.sharedApplication; + + return app.keyWindow.rootViewController; + } + + public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) { + return this.getViewController(); + } + + public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) { + return this.getViewController().view; + } + + public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect { + return this.getViewController().view.frame; + } + } +} diff --git a/nativescript-core/utils/utils-common.ts b/nativescript-core/utils/utils-common.ts index 8fe05c99f..f785482c4 100644 --- a/nativescript-core/utils/utils-common.ts +++ b/nativescript-core/utils/utils-common.ts @@ -1,7 +1,9 @@ import * as types from "./types"; import { dispatchToMainThread, isMainThread } from "./mainthread-helper"; import { sanitizeModuleName } from "../ui/builder/module-name-sanitizer"; +import * as layout from "./layout-helper"; +export { layout }; export * from "./mainthread-helper"; export const RESOURCE_PREFIX = "res://"; @@ -39,70 +41,6 @@ export function getModuleName(path: string): string { return sanitizeModuleName(moduleName); } -export module layoutCommon { - const MODE_SHIFT = 30; - const MODE_MASK = 0x3 << MODE_SHIFT; - - export const UNSPECIFIED = 0 << MODE_SHIFT; - export const EXACTLY = 1 << MODE_SHIFT; - export const AT_MOST = 2 << MODE_SHIFT; - - export const MEASURED_HEIGHT_STATE_SHIFT = 0x00000010; /* 16 */ - export const MEASURED_STATE_TOO_SMALL = 0x01000000; - export const MEASURED_STATE_MASK = 0xff000000; - export const MEASURED_SIZE_MASK = 0x00ffffff; - - export function getMode(mode: number): string { - switch (mode) { - case layoutCommon.EXACTLY: - return "Exact"; - case layoutCommon.AT_MOST: - return "AtMost"; - default: - return "Unspecified"; - } - } - - export function getMeasureSpecMode(spec: number): number { - return (spec & MODE_MASK); - } - - export function getMeasureSpecSize(spec: number): number { - return (spec & ~MODE_MASK); - } - - export function measureSpecToString(measureSpec: number): string { - const mode = getMeasureSpecMode(measureSpec); - const size = getMeasureSpecSize(measureSpec); - - let text = "MeasureSpec: "; - if (mode === UNSPECIFIED) { - text += "UNSPECIFIED "; - } else if (mode === EXACTLY) { - text += "EXACTLY "; - } else if (mode === AT_MOST) { - text += "AT_MOST "; - } - - text += size; - - return text; - } - - export function round(value: number): number { - const res = Math.floor(value + 0.5); - if (res !== 0) { - return res; - } else if (value === 0) { - return 0; - } else if (value > 0) { - return 1; - } - - return -1; - } -} - export function isFileOrResourcePath(path: string): boolean { if (!types.isString(path)) { return false; diff --git a/nativescript-core/utils/utils.android.ts b/nativescript-core/utils/utils.android.ts index 58bd26fc4..933b2e942 100644 --- a/nativescript-core/utils/utils.android.ts +++ b/nativescript-core/utils/utils.android.ts @@ -1,224 +1,17 @@ +import { ad } from "./native-helper"; +import { device } from "../platform"; +import { FileSystemAccess } from "../file-system/file-system-access"; import { write as traceWrite, categories as traceCategories, messageType as traceMessageType, } from "../trace"; -import { layoutCommon } from "./utils-common"; - +export { ad }; export * from "./utils-common"; -import { getNativeApplication, android as androidApp } from "../application"; -import { device } from "../platform"; -import { FileSystemAccess } from "../file-system/file-system-access"; - const MIN_URI_SHARE_RESTRICTED_APK_VERSION = 24; -export module layout { - let density: number; - - // cache the MeasureSpec constants here, to prevent extensive marshaling calls to and from Java - // TODO: While this boosts the performance it is error-prone in case Google changes these constants - const MODE_SHIFT = 30; - const MODE_MASK = 0x3 << MODE_SHIFT; - let sdkVersion: number; - let useOldMeasureSpec = false; - - export function makeMeasureSpec(size: number, mode: number): number { - if (sdkVersion === undefined) { - // check whether the old layout is needed - sdkVersion = ad.getApplicationContext().getApplicationInfo().targetSdkVersion; - useOldMeasureSpec = sdkVersion <= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; - } - - if (useOldMeasureSpec) { - return size + mode; - } - - return (size & ~MODE_MASK) | (mode & MODE_MASK); - } - - export function getDisplayDensity(): number { - if (density === undefined) { - density = ad.getResources().getDisplayMetrics().density; - } - - return density; - } - - export function toDevicePixels(value: number): number { - return value * getDisplayDensity(); - } - - export function toDeviceIndependentPixels(value: number): number { - return value / getDisplayDensity(); - } - - export function measureNativeView(nativeView: any /* android.view.View */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { - const view = nativeView; - view.measure(makeMeasureSpec(width, widthMode), makeMeasureSpec(height, heightMode)); - - return { - width: view.getMeasuredWidth(), - height: view.getMeasuredHeight() - }; - } -} - -// TODO(webpack-workflow): Export all methods from layoutCommon -// Think of a cleaner way to do that -Object.assign(layout, layoutCommon); - -// We are using "ad" here to avoid namespace collision with the global android object -export module ad { - - let application: android.app.Application; - let applicationContext: android.content.Context; - let contextResources: android.content.res.Resources; - let packageName: string; - export function getApplicationContext() { - if (!applicationContext) { - applicationContext = getApplication().getApplicationContext(); - } - - return applicationContext; - } - export function getApplication() { - if (!application) { - application = (getNativeApplication()); - } - - return application; - } - export function getResources() { - if (!contextResources) { - contextResources = getApplication().getResources(); - } - - return contextResources; - } - function getPackageName() { - if (!packageName) { - packageName = getApplicationContext().getPackageName(); - } - - return packageName; - } - - let inputMethodManager: android.view.inputmethod.InputMethodManager; - export function getInputMethodManager(): android.view.inputmethod.InputMethodManager { - if (!inputMethodManager) { - inputMethodManager = getApplicationContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); - } - - return inputMethodManager; - } - - export function showSoftInput(nativeView: android.view.View): void { - const inputManager = getInputMethodManager(); - if (inputManager && nativeView instanceof android.view.View) { - inputManager.showSoftInput(nativeView, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); - } - } - - export function dismissSoftInput(nativeView?: android.view.View): void { - const inputManager = getInputMethodManager(); - let windowToken: android.os.IBinder; - - if (nativeView instanceof android.view.View) { - windowToken = nativeView.getWindowToken(); - } else if (androidApp.foregroundActivity instanceof androidx.appcompat.app.AppCompatActivity) { - const decorView = androidApp.foregroundActivity.getWindow().getDecorView(); - windowToken = decorView ? decorView.getWindowToken() : null; - } - - if (inputManager && windowToken) { - inputManager.hideSoftInputFromWindow(windowToken, 0); - } - } - - export module collections { - export function stringArrayToStringSet(str: string[]): java.util.HashSet { - const hashSet = new java.util.HashSet(); - if (str !== undefined) { - for (let element in str) { - hashSet.add("" + str[element]); - } - } - - return hashSet; - } - - export function stringSetToStringArray(stringSet: any): string[] { - const arr = []; - if (stringSet !== undefined) { - const it = stringSet.iterator(); - while (it.hasNext()) { - const element = "" + it.next(); - arr.push(element); - } - } - - return arr; - } - } - - export module resources { - let attr; - const attrCache = new Map(); - - export function getDrawableId(name) { - return getId(":drawable/" + name); - } - - export function getStringId(name) { - return getId(":string/" + name); - } - - export function getId(name: string): number { - const resources = getResources(); - const packageName = getPackageName(); - const uri = packageName + name; - - return resources.getIdentifier(uri, null, null); - } - export function getPalleteColor(name: string, context: android.content.Context): number { - return getPaletteColor(name, context); - } - export function getPaletteColor(name: string, context: android.content.Context): number { - if (attrCache.has(name)) { - return attrCache.get(name); - } - - let result = 0; - try { - if (!attr) { - attr = java.lang.Class.forName("androidx.appcompat.R$attr"); - } - - let colorID = 0; - let field = attr.getField(name); - if (field) { - colorID = field.getInt(null); - } - - if (colorID) { - let typedValue = new android.util.TypedValue(); - context.getTheme().resolveAttribute(colorID, typedValue, true); - result = typedValue.data; - } - } - catch (ex) { - traceWrite("Cannot get pallete color: " + name, traceCategories.Error, traceMessageType.error); - } - - attrCache.set(name, result); - - return result; - } - } -} - export function GC() { gc(); } diff --git a/nativescript-core/utils/utils.ios.ts b/nativescript-core/utils/utils.ios.ts index 5c075eb73..17474fd8d 100644 --- a/nativescript-core/utils/utils.ios.ts +++ b/nativescript-core/utils/utils.ios.ts @@ -1,152 +1,20 @@ +import { ios } from "./native-helper"; import { write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../trace"; -import { layoutCommon } from "./utils-common"; +export { ios }; export * from "./utils-common"; let mainScreenScale; -function isOrientationLandscape(orientation: number) { - return orientation === UIDeviceOrientation.LandscapeLeft /* 3 */ || - orientation === UIDeviceOrientation.LandscapeRight /* 4 */; -} - -export module layout { - const MODE_SHIFT = 30; - const MODE_MASK = 0x3 << MODE_SHIFT; - - export function makeMeasureSpec(size: number, mode: number): number { - return (Math.round(Math.max(0, size)) & ~MODE_MASK) | (mode & MODE_MASK); - } - - export function getDisplayDensity(): number { - return mainScreenScale; - } - - export function toDevicePixels(value: number): number { - return value * mainScreenScale; - } - - export function toDeviceIndependentPixels(value: number): number { - return value / mainScreenScale; - } - - export function measureNativeView(nativeView: any /* UIView */, width: number, widthMode: number, height: number, heightMode: number): { width: number, height: number } { - const view = nativeView; - const nativeSize = view.sizeThatFits({ - width: widthMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(width), - height: heightMode === 0 /* layout.UNSPECIFIED */ ? Number.POSITIVE_INFINITY : toDeviceIndependentPixels(height) - }); - - nativeSize.width = layoutCommon.round(toDevicePixels(nativeSize.width)); - nativeSize.height = layoutCommon.round(toDevicePixels(nativeSize.height)); - - return nativeSize; - } -} - -// TODO(webpack-workflow): Export all methods from layoutCommon -// Think of a cleaner way to do that -Object.assign(layout, layoutCommon); - -export module ios { - // TODO: remove for NativeScript 7.0 - export function getter(_this: any, property: T | { (): T }): T { - console.log("utils.ios.getter() is deprecated; use the respective native property instead"); - if (typeof property === "function") { - return (<{ (): T }>property).call(_this); - } else { - return property; - } - } - - export module collections { - export function jsArrayToNSArray(str: string[]): NSArray { - return NSArray.arrayWithArray(str); - } - - export function nsArrayToJSArray(a: NSArray): Array { - const arr = []; - if (a !== undefined) { - let count = a.count; - for (let i = 0; i < count; i++) { - arr.push(a.objectAtIndex(i)); - } - } - - return arr; - } - } - - export function isLandscape(): boolean { - console.log("utils.ios.isLandscape() is deprecated; use application.orientation instead"); - - const deviceOrientation = UIDevice.currentDevice.orientation; - const statusBarOrientation = UIApplication.sharedApplication.statusBarOrientation; - - const isDeviceOrientationLandscape = isOrientationLandscape(deviceOrientation); - const isStatusBarOrientationLandscape = isOrientationLandscape(statusBarOrientation); - - return isDeviceOrientationLandscape || isStatusBarOrientationLandscape; - } - - export const MajorVersion = NSString.stringWithString(UIDevice.currentDevice.systemVersion).intValue; - - export function openFile(filePath: string): boolean { - console.log("utils.ios.openFile() is deprecated; use utils.openFile() instead"); - - return openFileAtRootModule(filePath); - } - - export function getCurrentAppPath(): string { - const currentDir = __dirname; - const tnsModulesIndex = currentDir.indexOf("/tns_modules"); - - // Module not hosted in ~/tns_modules when bundled. Use current dir. - let appPath = currentDir; - if (tnsModulesIndex !== -1) { - // Strip part after tns_modules to obtain app root - appPath = currentDir.substring(0, tnsModulesIndex); - } - - return appPath; - } - - export function joinPaths(...paths: string[]): string { - if (!paths || paths.length === 0) { - return ""; - } - - return NSString.stringWithString(NSString.pathWithComponents(paths)).stringByStandardizingPath; - } - - export function getVisibleViewController(rootViewController: UIViewController): UIViewController { - if (rootViewController.presentedViewController) { - return getVisibleViewController(rootViewController.presentedViewController); - } - - if (rootViewController.isKindOfClass(UINavigationController.class())) { - return getVisibleViewController((rootViewController).visibleViewController); - } - - if (rootViewController.isKindOfClass(UITabBarController.class())) { - return getVisibleViewController(rootViewController); - } - - return rootViewController; - - } - -} - export function openFile(filePath: string): boolean { try { const appPath = ios.getCurrentAppPath(); const path = filePath.replace("~", appPath); const controller = UIDocumentInteractionController.interactionControllerWithURL(NSURL.fileURLWithPath(path)); - controller.delegate = new UIDocumentInteractionControllerDelegateImpl(); + controller.delegate = new ios.UIDocumentInteractionControllerDelegateImpl(); return controller.presentPreviewAnimated(true); } @@ -157,9 +25,6 @@ export function openFile(filePath: string): boolean { return false; } -// Need this so that we can use this function inside the ios module (avoid name clashing). -const openFileAtRootModule = openFile; - export function GC() { __collect(); } @@ -183,26 +48,4 @@ export function openUrl(location: string): boolean { return false; } -class UIDocumentInteractionControllerDelegateImpl extends NSObject implements UIDocumentInteractionControllerDelegate { - public static ObjCProtocols = [UIDocumentInteractionControllerDelegate]; - - public getViewController(): UIViewController { - const app = UIApplication.sharedApplication; - - return app.keyWindow.rootViewController; - } - - public documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) { - return this.getViewController(); - } - - public documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController) { - return this.getViewController().view; - } - - public documentInteractionControllerRectForPreview(controller: UIDocumentInteractionController): CGRect { - return this.getViewController().view.frame; - } -} - mainScreenScale = UIScreen.mainScreen.scale; diff --git a/tests/webpack.config.js b/tests/webpack.config.js index a6f7b7bb3..e3f351435 100644 --- a/tests/webpack.config.js +++ b/tests/webpack.config.js @@ -13,7 +13,7 @@ const TerserPlugin = require("terser-webpack-plugin"); const hashSalt = Date.now().toString(); const ANDROID_MAX_CYCLES = 66; -const IOS_MAX_CYCLES = 39; +const IOS_MAX_CYCLES = 32; let numCyclesDetected = 0; module.exports = env => { diff --git a/unit-tests/runtime.ts b/unit-tests/runtime.ts index 1cbff0440..961484e15 100644 --- a/unit-tests/runtime.ts +++ b/unit-tests/runtime.ts @@ -11,6 +11,7 @@ moduleAlias.addAliases({ "@nativescript/core/platform": path.resolve(__dirname, "polyfills", "platform"), "@nativescript/core/file-system/file-system-access": path.resolve(__dirname, "polyfills", "file-system-access"), "@nativescript/core/utils/utils": path.resolve(tnsCoreModules, "utils/utils-common"), + "./layout-helper": path.resolve(tnsCoreModules, "utils/layout-helper/layout-helper-common"), "./mainthread-helper": path.resolve(__dirname, "polyfills", "mainthread-helper"), "@nativescript/core/color": path.resolve(tnsCoreModules, "color/color-common"), "@nativescript/core/ui/styling/font": path.resolve(tnsCoreModules, "ui/styling/font-common"),