diff --git a/api-reports/NativeScript.api.md b/api-reports/NativeScript.api.md
index 31e464aac1..44fc07a7f8 100644
--- a/api-reports/NativeScript.api.md
+++ b/api-reports/NativeScript.api.md
@@ -2839,6 +2839,7 @@ export abstract class ViewBase extends Observable {
// (undocumented)
_defaultPaddingTop: number;
public deletePseudoClass(name: string): void;
+ destroyNode(forceDestroyChildren?: boolean): void;
public _dialogClosed(): void;
disposeNativeView(): void;
// (undocumented)
@@ -2955,6 +2956,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/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..95624b04cc
--- /dev/null
+++ b/e2e/ui-tests-app/app/issues/issue-7469-page.ts
@@ -0,0 +1,144 @@
+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";
+import { Label } from "tns-core-modules/ui/label/label";
+
+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();
+}
+
+let timeouts = [];
+let intervals = [];
+
+let reusableItem;
+let loaded = false;
+let isIn1 = false;
+
+export function pageLoaded(args) {
+ startFPSMeter();
+ if (loaded) {
+ fpsLabel = null;
+ // stopFPSMeter();
+ timeouts.forEach((v) => clearTimeout(v));
+ intervals.forEach((v) => clearInterval(v));
+ reusableItem._tearDownUI(true);
+ }
+ loaded = true;
+ reusableItem = args.object.getViewById("reusableItem");
+ fpsLabel = args.object.getViewById("fpslabel");
+ const stack1: StackLayout = args.object.getViewById("stack1");
+ const stack2: StackLayout = args.object.getViewById("stack2");
+ setTimeout(() => {
+ // label.android.setTextColor(new Color("red").android);
+ // label.android.setBackgroundColor(new Color("red").android);
+ startFPSMeter();
+ console.log("setRed");
+ }, 1000);
+ // console.log(label._context);
+ // isIn1 = false;
+ // timeouts.push(setTimeout(() => {
+ // intervals.push(setInterval(() => {
+ // label.parent.removeChild(label);
+ // // console.log(label.nativeView);
+ // if(isIn1) {
+ // isIn1 = false;
+ // stack2.addChild(label);
+ // } else {
+ // isIn1 = true;
+ // stack1.addChild(label);
+ // }
+ // }, 10));
+ // }, 1001));
+}
+
+export function pageUnloaded(args) {
+ //
+}
+
+export function makeReusable(args: EventData) {
+ console.log("loaded:", args.object);
+ // console.log("making reusable");
+ if ((args.object as any).___reusableRan) {
+ return;
+ }
+ (args.object as any).___reusableRan = true;
+ (args.object as any).reusable = true;
+}
+
+export function onReusableUnloaded(args: EventData) {
+ console.log("unloaded:", args.object);
+}
+var testLabel: Label;
+
+export function test(args: any) {
+ const page = args.object.page;
+ reusableItem = page.getViewById("reusableItem");
+ const stack1: StackLayout = page.getViewById("stack1");
+ const stack2: StackLayout = page.getViewById("stack2");
+ if (!testLabel) {
+ testLabel = new Label();
+ testLabel.text = "This label is not reusable and is dynamic";
+ testLabel.on("loaded", () => { console.log("LODADED testLabel"); });
+ testLabel.on("unloaded", () => { console.log("UNLODADED testLabel"); });
+ }
+ reusableItem.parent.removeChild(reusableItem);
+ if (!reusableItem._suspendNativeUpdatesCount) {
+ console.log("reusableItem SHOULD BE UNLOADED");
+ }
+ if (!testLabel._suspendNativeUpdatesCount) {
+ console.log("testLabel SHOULD BE UNLOADED");
+ }
+ if (!testLabel.parent) {
+ reusableItem.addChild(testLabel);
+ }
+ if (!testLabel.nativeView) {
+ console.log("testLabel NATIVE VIEW SHOULD BE CREATED");
+ }
+ if (!testLabel._suspendNativeUpdatesCount) {
+ console.log("testLabel SHOULD BE UNLOADED");
+ }
+ if (isIn1) {
+ isIn1 = false;
+ stack2.addChild(reusableItem);
+ } else {
+ isIn1 = true;
+ stack1.addChild(reusableItem);
+ }
+ if (reusableItem._suspendNativeUpdatesCount) {
+ console.log("reusableItem SHOULD BE LOADED AND RECEIVING UPDATES");
+ }
+ if (testLabel._suspendNativeUpdatesCount) {
+ console.log("testLabel SHOULD BE LOADED AND RECEIVING UPDATES");
+ }
+ // console.log("onTap");
+ // alert("onTap");
+}
+let ignoreInput = false;
+
+export function toggleReusable(args: EventData) {
+ if (ignoreInput) {
+ return;
+ }
+ ignoreInput = true;
+ setTimeout(() => ignoreInput = false, 0); // hack to avoid gesture collision
+ const target: any = args.object;
+ target.reusable = !target.reusable;
+ console.log(`${target} is now ${target.reusable ? "" : "NOT "}reusable`);
+}
\ No newline at end of file
diff --git a/e2e/ui-tests-app/app/issues/issue-7469-page.xml b/e2e/ui-tests-app/app/issues/issue-7469-page.xml
new file mode 100644
index 0000000000..f48945eaa3
--- /dev/null
+++ b/e2e/ui-tests-app/app/issues/issue-7469-page.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/ui-tests-app/app/issues/main-page.ts b/e2e/ui-tests-app/app/issues/main-page.ts
index 3b03c492a7..29691850cb 100644
--- a/e2e/ui-tests-app/app/issues/main-page.ts
+++ b/e2e/ui-tests-app/app/issues/main-page.ts
@@ -11,6 +11,7 @@ export function pageLoaded(args: EventData) {
export function loadExamples() {
const examples = new Map();
+ examples.set("7469", "issues/issue-7469-page");
examples.set("2911", "issues/issue-2911-page");
examples.set("2674", "issues/issue-2674-page");
examples.set("2942", "issues/issue-2942-page");
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 c6b0a9a244..371e3df989 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".
*/
@@ -369,6 +375,13 @@ export abstract class ViewBase extends Observable {
* This method should *not* be overridden by derived views.
*/
_tearDownUI(force?: boolean): void;
+
+ /**
+ * Tears down the UI of a reusable node by making it no longer reusable.
+ * @see _tearDownUI
+ * @param forceDestroyChildren Force destroy the children (even if they are reusable)
+ */
+ destroyNode(forceDestroyChildren?: boolean): void;
/**
* Creates a native view.
diff --git a/nativescript-core/ui/core/view-base/view-base.ts b/nativescript-core/ui/core/view-base/view-base.ts
index 77bdb0e11b..5c295ebec7 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);
@@ -254,6 +254,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _moduleName: string;
+ public reusable: boolean;
+
constructor() {
super();
this._domId = viewIdCounter++;
@@ -717,6 +719,11 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public _setupUI(context: android.content.Context, atIndex?: number, parentIsLoaded?: boolean): void {
if (this._context === context) {
+ if (this.parent) {
+ const nativeIndex = this.parent._childIndexToNativeChildIndex(atIndex);
+ this._isAddedToNativeVisualTree = this.parent._addViewToNativeVisualTree(this, nativeIndex);
+ }
+
return;
} else if (this._context) {
this._tearDownUI(true);
@@ -738,7 +745,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) {
@@ -770,7 +777,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.effectivePaddingLeft = this._defaultPaddingLeft;
}
}
- } else {
+ } else if (isIOS) {
this._iosView = nativeView;
}
@@ -808,20 +815,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);
@@ -846,18 +861,20 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
// }
// }
- this.disposeNativeView();
+ if (!preserveNativeView) {
+ 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();
@@ -1049,6 +1066,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 |