diff --git a/apps/ui/src/issues/issue-7469-page.css b/apps/ui/src/issues/issue-7469-page.css
new file mode 100644
index 000000000..dd2685f0d
--- /dev/null
+++ b/apps/ui/src/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;
+ }
\ No newline at end of file
diff --git a/apps/ui/src/issues/issue-7469-page.ts b/apps/ui/src/issues/issue-7469-page.ts
new file mode 100644
index 000000000..eca14bb6a
--- /dev/null
+++ b/apps/ui/src/issues/issue-7469-page.ts
@@ -0,0 +1,158 @@
+import { EventData, Label, StackLayout } from '@nativescript/core';
+import { addCallback, removeCallback, start, stop } from '@nativescript/core/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();
+}
+
+let timeouts = [];
+let intervals = [];
+
+let reusableItem;
+let vcToggle;
+let loaded = false;
+let isIn1 = false;
+
+function updateVcToggleText() {
+ vcToggle.text = `Container is${reusableItem.reusable ? ' ' : ' NOT '}Reusable`
+}
+
+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');
+ vcToggle = args.object.getViewById('vcToggle');
+ updateVcToggleText();
+ 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;
+ if(args.object === reusableItem) {
+ updateVcToggleText();
+ }
+}
+
+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`);
+}
+
+export function toggleVCReusable() {
+ reusableItem.reusable = !reusableItem.reusable;
+ updateVcToggleText();
+}
diff --git a/apps/ui/src/issues/issue-7469-page.xml b/apps/ui/src/issues/issue-7469-page.xml
new file mode 100644
index 000000000..ed1797c68
--- /dev/null
+++ b/apps/ui/src/issues/issue-7469-page.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/ui/src/issues/main-page.ts b/apps/ui/src/issues/main-page.ts
index c62b20fb0..73dfbd0bf 100644
--- a/apps/ui/src/issues/main-page.ts
+++ b/apps/ui/src/issues/main-page.ts
@@ -32,6 +32,7 @@ export function loadExamples() {
examples.set('ng-repo-1626', 'issues/issue-ng-repo-1626-page');
examples.set('6439', 'issues/issue-6439-page');
examples.set('open-file-6895', 'issues/open-file-6895-page');
+ examples.set("7469", "issues/issue-7469-page");
return examples;
}
diff --git a/packages/core/ui/core/view-base/index.d.ts b/packages/core/ui/core/view-base/index.d.ts
index 1c88ea81b..a7eb05a2f 100644
--- a/packages/core/ui/core/view-base/index.d.ts
+++ b/packages/core/ui/core/view-base/index.d.ts
@@ -236,6 +236,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".
*/
@@ -365,6 +371,13 @@ export abstract class ViewBase extends Observable {
*/
_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.
* Returns either android.view.View or UIView.
diff --git a/packages/core/ui/core/view-base/index.ts b/packages/core/ui/core/view-base/index.ts
index ccf7d26f6..1f9ad7ffa 100644
--- a/packages/core/ui/core/view-base/index.ts
+++ b/packages/core/ui/core/view-base/index.ts
@@ -307,6 +307,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _moduleName: string;
+ public reusable: boolean;
+
constructor() {
super();
this._domId = viewIdCounter++;
@@ -767,6 +769,14 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile
public _setupUI(context: any, atIndex?: number, parentIsLoaded?: boolean): void {
if (this._context === context) {
+ // this check is unnecessary as this function should never be called when this._context === context as it means the view was somehow detached,
+ // which is only possible by setting reusable = true. Adding it either way for feature flag safety
+ if (this.reusable) {
+ if (this.parent && !this._isAddedToNativeVisualTree) {
+ const nativeIndex = this.parent._childIndexToNativeChildIndex(atIndex);
+ this._isAddedToNativeVisualTree = this.parent._addViewToNativeVisualTree(this, nativeIndex);
+ }
+ }
return;
} else if (this._context) {
this._tearDownUI(true);
@@ -789,35 +799,39 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
}
if (global.isAndroid) {
- this._androidView = nativeView;
- if (nativeView) {
- if (this._isPaddingRelative === undefined) {
- this._isPaddingRelative = nativeView.isPaddingRelative();
- }
+ // this check is also unecessary as this code should never be reached with _androidView != null unless reusable = true
+ // also adding this check for feature flag safety
+ if (this._androidView !== nativeView || !this.reusable) {
+ this._androidView = nativeView;
+ if (nativeView) {
+ if (this._isPaddingRelative === undefined) {
+ this._isPaddingRelative = nativeView.isPaddingRelative();
+ }
- let result: any /* android.graphics.Rect */ = (nativeView).defaultPaddings;
- if (result === undefined) {
- result = org.nativescript.widgets.ViewHelper.getPadding(nativeView);
- (nativeView).defaultPaddings = result;
- }
+ let result: any /* android.graphics.Rect */ = (nativeView).defaultPaddings;
+ if (result === undefined) {
+ result = org.nativescript.widgets.ViewHelper.getPadding(nativeView);
+ (nativeView).defaultPaddings = result;
+ }
- this._defaultPaddingTop = result.top;
- this._defaultPaddingRight = result.right;
- this._defaultPaddingBottom = result.bottom;
- this._defaultPaddingLeft = result.left;
+ this._defaultPaddingTop = result.top;
+ this._defaultPaddingRight = result.right;
+ this._defaultPaddingBottom = result.bottom;
+ this._defaultPaddingLeft = result.left;
- const style = this.style;
- if (!paddingTopProperty.isSet(style)) {
- this.effectivePaddingTop = this._defaultPaddingTop;
- }
- if (!paddingRightProperty.isSet(style)) {
- this.effectivePaddingRight = this._defaultPaddingRight;
- }
- if (!paddingBottomProperty.isSet(style)) {
- this.effectivePaddingBottom = this._defaultPaddingBottom;
- }
- if (!paddingLeftProperty.isSet(style)) {
- this.effectivePaddingLeft = this._defaultPaddingLeft;
+ const style = this.style;
+ if (!paddingTopProperty.isSet(style)) {
+ this.effectivePaddingTop = this._defaultPaddingTop;
+ }
+ if (!paddingRightProperty.isSet(style)) {
+ this.effectivePaddingRight = this._defaultPaddingRight;
+ }
+ if (!paddingBottomProperty.isSet(style)) {
+ this.effectivePaddingBottom = this._defaultPaddingBottom;
+ }
+ if (!paddingLeftProperty.isSet(style)) {
+ this.effectivePaddingLeft = this._defaultPaddingLeft;
+ }
}
}
} else {
@@ -858,20 +872,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);
@@ -896,19 +918,21 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
// }
// }
- this.disposeNativeView();
+ if (!preserveNativeView) {
+ this.disposeNativeView();
- this._suspendNativeUpdates(SuspendType.UISetup);
+ this._suspendNativeUpdates(SuspendType.UISetup);
- if (global.isAndroid) {
- this.setNativeView(null);
- this._androidView = null;
+ if (global.isAndroid) {
+ this.setNativeView(null);
+ this._androidView = null;
+ }
+
+ // this._iosView = null;
+
+ this._context = null;
}
- // this._iosView = null;
-
- this._context = null;
-
if (this.domNode) {
this.domNode.dispose();
this.domNode = undefined;
@@ -1099,6 +1123,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 | SuspendType.NativeView | SuspendType.UISetup;