From 96df3f29cc46b761f36a396cfe134c31fede0cb1 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 9 Dec 2019 12:40:12 -0300 Subject: [PATCH 1/7] feat: views can be marked as reusable Reusable views have to be teared down manually. --- api-reports/NativeScript.api.md | 1 + .../ui/core/view-base/view-base.d.ts | 6 ++ .../ui/core/view-base/view-base.ts | 59 +++++++++++++------ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md index d2d7d93604..eb2e83bf6f 100644 --- a/api-reports/NativeScript.api.md +++ b/api-reports/NativeScript.api.md @@ -2904,6 +2904,7 @@ export abstract class ViewBase extends Observable { _removeViewFromNativeVisualTree(view: ViewBase): void; public requestLayout(): void; resetNativeView(): void; + public reusable: boolean; // (undocumented) row: number; // (undocumented) 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 0537ac47e3..82ba726f94 100644 --- a/nativescript-core/ui/core/view-base/view-base.d.ts +++ b/nativescript-core/ui/core/view-base/view-base.d.ts @@ -241,6 +241,12 @@ export abstract class ViewBase extends Observable { public nativeView: any; public bindingContext: any; + /** + * Gets or sets if the view is reusable. + * Reusable views are not automatically destroyed when removed from the View tree. + */ + public reusable: boolean; + /** * Gets the name of the constructor function for this instance. E.g. for a Button class this will return "Button". */ diff --git a/nativescript-core/ui/core/view-base/view-base.ts b/nativescript-core/ui/core/view-base/view-base.ts index 092eec6fcf..5752869fe0 100644 --- a/nativescript-core/ui/core/view-base/view-base.ts +++ b/nativescript-core/ui/core/view-base/view-base.ts @@ -72,7 +72,7 @@ export function getViewById(view: ViewBaseDefinition, id: string): ViewBaseDefin } let retVal: ViewBaseDefinition; - const descendantsCallback = function (child: ViewBaseDefinition): boolean { + const descendantsCallback = function(child: ViewBaseDefinition): boolean { if (child.id === id) { retVal = child; @@ -94,7 +94,7 @@ export function eachDescendant(view: ViewBaseDefinition, callback: (child: ViewB } let continueIteration: boolean; - let localCallback = function (child: ViewBaseDefinition): boolean { + let localCallback = function(child: ViewBaseDefinition): boolean { continueIteration = callback(child); if (continueIteration) { child.eachChild(localCallback); @@ -253,6 +253,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition public _moduleName: string; + public reusable: boolean; + constructor() { super(); this._domId = viewIdCounter++; @@ -700,6 +702,16 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition @profile public _setupUI(context: android.content.Context, atIndex?: number, parentIsLoaded?: boolean): void { if (this._context === context) { + this._resumeNativeUpdates(SuspendType.UISetup); + if (this.parent) { + const nativeIndex = this.parent._childIndexToNativeChildIndex(atIndex); + this._isAddedToNativeVisualTree = this.parent._addViewToNativeVisualTree(this, nativeIndex); + } + this.eachChild((child) => { + child._setupUI(context); + + return true; + }); return; } else if (this._context) { this._tearDownUI(true); @@ -721,7 +733,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition nativeView = this.createNativeView(); } - if (isAndroid) { + if (isAndroid && this._androidView !== nativeView) { this._androidView = nativeView; if (nativeView) { if (this._isPaddingRelative === undefined) { @@ -753,7 +765,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition this.effectivePaddingLeft = this._defaultPaddingLeft; } } - } else { + } else if (isIOS) { this._iosView = nativeView; } @@ -791,20 +803,28 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition } } + public destroyNode(forceDestroyChildren?: boolean): void { + this.reusable = false; + this._tearDownUI(forceDestroyChildren); + } + @profile public _tearDownUI(force?: boolean): void { // No context means we are already teared down. if (!this._context) { return; } + const preserveNativeView = this.reusable && !force; this.resetNativeViewInternal(); - this.eachChild((child) => { - child._tearDownUI(force); + if (!preserveNativeView) { + this.eachChild((child) => { + child._tearDownUI(force); - return true; - }); + return true; + }); + } if (this.parent) { this.parent._removeViewFromNativeVisualTree(this); @@ -829,18 +849,22 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition // } // } - this.disposeNativeView(); + if (preserveNativeView) { + this._suspendNativeUpdates(SuspendType.UISetup); + } else { + this.disposeNativeView(); - this._suspendNativeUpdates(SuspendType.UISetup); + this._suspendNativeUpdates(SuspendType.UISetup); - if (isAndroid) { - this.setNativeView(null); - this._androidView = null; - } + if (isAndroid) { + this.setNativeView(null); + this._androidView = null; + } - // this._iosView = null; + // this._iosView = null; - this._context = null; + this._context = null; + } if (this.domNode) { this.domNode.dispose(); @@ -1032,6 +1056,7 @@ ViewBase.prototype._defaultPaddingBottom = 0; ViewBase.prototype._defaultPaddingLeft = 0; ViewBase.prototype._isViewBase = true; ViewBase.prototype.recycleNativeView = "never"; +ViewBase.prototype.reusable = false; ViewBase.prototype._suspendNativeUpdatesCount = SuspendType.Loaded | @@ -1053,7 +1078,7 @@ export const classNameProperty = new Property({ cssClasses.clear(); if (shouldAddModalRootViewCssClasses) { - cssClasses.add(MODAL_ROOT_VIEW_CSS_CLASS); + cssClasses.add(MODAL_ROOT_VIEW_CSS_CLASS); } else if (shouldAddRootViewCssClasses) { cssClasses.add(ROOT_VIEW_CSS_CLASS); } From 4a0072e7efc9ed2236917f3a6e70b0ddca07b23d Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 9 Dec 2019 12:40:46 -0300 Subject: [PATCH 2/7] chore: add reusable view demo --- .../app/issues/issue-7469-page.css | 15 +++ .../app/issues/issue-7469-page.ts | 109 ++++++++++++++++++ .../app/issues/issue-7469-page.xml | 35 ++++++ e2e/ui-tests-app/app/issues/main-page.ts | 1 + 4 files changed, 160 insertions(+) create mode 100644 e2e/ui-tests-app/app/issues/issue-7469-page.css create mode 100644 e2e/ui-tests-app/app/issues/issue-7469-page.ts create mode 100644 e2e/ui-tests-app/app/issues/issue-7469-page.xml diff --git a/e2e/ui-tests-app/app/issues/issue-7469-page.css b/e2e/ui-tests-app/app/issues/issue-7469-page.css new file mode 100644 index 0000000000..cf48b550a5 --- /dev/null +++ b/e2e/ui-tests-app/app/issues/issue-7469-page.css @@ -0,0 +1,15 @@ +.test-label { + padding: 10; + background-color: black; + color: white; +} + +.stack1 { + background-color: green; + color: white; +} + +.stack2 { + background-color: blue; + color: red; +} diff --git a/e2e/ui-tests-app/app/issues/issue-7469-page.ts b/e2e/ui-tests-app/app/issues/issue-7469-page.ts new file mode 100644 index 0000000000..08b7beec51 --- /dev/null +++ b/e2e/ui-tests-app/app/issues/issue-7469-page.ts @@ -0,0 +1,109 @@ +import { EventData } from "tns-core-modules/data/observable"; +import { Page, Color } from "tns-core-modules/ui/page"; +import { Button } from "tns-core-modules/ui/button"; +import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout"; +import { removeCallback, start, stop, addCallback } from "tns-core-modules/fps-meter"; + +let callbackId; +let fpsLabel: any; +export function startFPSMeter() { + callbackId = addCallback((fps: number, minFps: number) => { + // console.log(`Frames per seconds: ${fps.toFixed(2)}`); + // console.log(minFps.toFixed(2)); + if(fpsLabel) { + fpsLabel.text = `${fps}`; + } + + }); + start(); +} + +export function stopFPSMeter() { + removeCallback(callbackId); + stop(); +} + +export function toggle(args: EventData) { + const page = ((args.object).page); + + const getElementById = id => page.getViewById(id); + + const toggleBtn =