8000 DO NOT MERGE: feat: reusable views by edusperoni · Pull Request #8184 · NativeScript/NativeScript · GitHub
[go: up one dir, main page]

Skip to content

DO NOT MERGE: feat: reusable views #8184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
2 changes: 2 additions & 0 deletions api-reports/NativeScript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions e2e/ui-tests-app/app/issues/issue-7469-page.css
Original file line number Diff line number Diff line change
@@ -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;
}
144 changes: 144 additions & 0 deletions e2e/ui-tests-app/app/issues/issue-7469-page.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
36 changes: 36 additions & 0 deletions e2e/ui-tests-app/app/issues/issue-7469-page.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Page
xmlns="http://schemas.nativescript.org/tns.xsd" class="page" loaded="pageLoaded" unloaded="pageUnloaded">
<StackLayout class=" 67F4 p-20">
<Button text="Swap locations" tap="test"/>
<Label text="Longpress items to toggle reusability"></Label>
<Label id="fpslabel" text=""></Label>
<StackLayout longPress="toggleReusable" id="reusableItem" loaded="makeReusable">
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<Label longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" text="abc"></Label>
<WebView longPress="toggleReusable" loaded="makeReusable" unloaded="onReusableUnloaded" width="100" height="100" src="https://google.com"></WebView>
</StackLayout>
<StackLayout id="stack1" class="stack1">
<Label text="Stack 1"></Label>
</StackLayout>
<StackLayout id="stack2" class="stack2">
<Label text="Stack 2"></Label>
</StackLayout>
</StackLayout>
</Page>
1 change: 1 addition & 0 deletions e2e/ui-tests-app/app/issu A3D4 es/main-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function pageLoaded(args: EventData) {

export function loadExamples() {
const examples = new Map<string, string>();
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");
Expand Down
13 changes: 13 additions & 0 deletions nativescript-core/ui/core/view-base/view-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".
*/
Expand Down Expand Up @@ -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.
Expand Down
50 changes: 34 additions & 16 deletions nativescript-core/ui/core/view-base/view-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -254,6 +254,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition

public _moduleName: string;

public reusable: boolean;

constructor() {
super();
this._domId = viewIdCounter++;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -770,7 +777,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
this.effectivePaddingLeft = this._defaultPaddingLeft;
}
}
} else {
} else if (isIOS) {
this._iosView = nativeView;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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 |
Expand Down
0