feat(core): reusable views (#9163)

This commit is contained in:
Eduardo Speroni
2021-01-29 16:11:52 -03:00
committed by Nathan Walker
parent e9e4934faf
commit 6cc130fa6f
6 changed files with 288 additions and 39 deletions

View File

@ -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;
}

View File

@ -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();
}

View File

@ -0,0 +1,37 @@
<Page
xmlns="http://schemas.nativescript.org/tns.xsd" class="page" loaded="pageLoaded" unloaded="pageUnloaded">
<StackLayout class="p-20">
<Button text="Swap locations" tap="test"/>
<Button id="vcToggle" text="" tap="toggleVCReusable"/>
<Label text="Longpress items to toggle reusability"></Label>
<Label id="fpslabel" text=""></Label>
<StackLayout 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>

View File

@ -32,6 +32,7 @@ export function loadExamples() {
examples.set('ng-repo-1626', 'issues/issue-ng-repo-1626-page'); examples.set('ng-repo-1626', 'issues/issue-ng-repo-1626-page');
examples.set('6439', 'issues/issue-6439-page'); examples.set('6439', 'issues/issue-6439-page');
examples.set('open-file-6895', 'issues/open-file-6895-page'); examples.set('open-file-6895', 'issues/open-file-6895-page');
examples.set("7469", "issues/issue-7469-page");
return examples; return examples;
} }

View File

@ -236,6 +236,12 @@ export abstract class ViewBase extends Observable {
public nativeView: any; public nativeView: any;
public bindingContext: 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". * 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; _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. * Creates a native view.
* Returns either android.view.View or UIView. * Returns either android.view.View or UIView.

View File

@ -307,6 +307,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
public _moduleName: string; public _moduleName: string;
public reusable: boolean;
constructor() { constructor() {
super(); super();
this._domId = viewIdCounter++; this._domId = viewIdCounter++;
@ -767,6 +769,14 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
@profile @profile
public _setupUI(context: any, atIndex?: number, parentIsLoaded?: boolean): void { public _setupUI(context: any, atIndex?: number, parentIsLoaded?: boolean): void {
if (this._context === context) { 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; return;
} else if (this._context) { } else if (this._context) {
this._tearDownUI(true); this._tearDownUI(true);
@ -789,35 +799,39 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
} }
if (global.isAndroid) { if (global.isAndroid) {
this._androidView = nativeView; // this check is also unecessary as this code should never be reached with _androidView != null unless reusable = true
if (nativeView) { // also adding this check for feature flag safety
if (this._isPaddingRelative === undefined) { if (this._androidView !== nativeView || !this.reusable) {
this._isPaddingRelative = nativeView.isPaddingRelative(); this._androidView = nativeView;
} if (nativeView) {
if (this._isPaddingRelative === undefined) {
this._isPaddingRelative = nativeView.isPaddingRelative();
}
let result: any /* android.graphics.Rect */ = (<any>nativeView).defaultPaddings; let result: any /* android.graphics.Rect */ = (<any>nativeView).defaultPaddings;
if (result === undefined) { if (result === undefined) {
result = org.nativescript.widgets.ViewHelper.getPadding(nativeView); result = org.nativescript.widgets.ViewHelper.getPadding(nativeView);
(<any>nativeView).defaultPaddings = result; (<any>nativeView).defaultPaddings = result;
} }
this._defaultPaddingTop = result.top; this._defaultPaddingTop = result.top;
this._defaultPaddingRight = result.right; this._defaultPaddingRight = result.right;
this._defaultPaddingBottom = result.bottom; this._defaultPaddingBottom = result.bottom;
this._defaultPaddingLeft = result.left; this._defaultPaddingLeft = result.left;
const style = this.style; const style = this.style;
if (!paddingTopProperty.isSet(style)) { if (!paddingTopProperty.isSet(style)) {
this.effectivePaddingTop = this._defaultPaddingTop; this.effectivePaddingTop = this._defaultPaddingTop;
} }
if (!paddingRightProperty.isSet(style)) { if (!paddingRightProperty.isSet(style)) {
this.effectivePaddingRight = this._defaultPaddingRight; this.effectivePaddingRight = this._defaultPaddingRight;
} }
if (!paddingBottomProperty.isSet(style)) { if (!paddingBottomProperty.isSet(style)) {
this.effectivePaddingBottom = this._defaultPaddingBottom; this.effectivePaddingBottom = this._defaultPaddingBottom;
} }
if (!paddingLeftProperty.isSet(style)) { if (!paddingLeftProperty.isSet(style)) {
this.effectivePaddingLeft = this._defaultPaddingLeft; this.effectivePaddingLeft = this._defaultPaddingLeft;
}
} }
} }
} else { } 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 @profile
public _tearDownUI(force?: boolean): void { public _tearDownUI(force?: boolean): void {
// No context means we are already teared down. // No context means we are already teared down.
if (!this._context) { if (!this._context) {
return; return;
} }
const preserveNativeView = this.reusable && !force;
this.resetNativeViewInternal(); this.resetNativeViewInternal();
this.eachChild((child) => { if (!preserveNativeView) {
child._tearDownUI(force); this.eachChild((child) => {
child._tearDownUI(force);
return true; return true;
}); });
}
if (this.parent) { if (this.parent) {
this.parent._removeViewFromNativeVisualTree(this); 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) { if (global.isAndroid) {
this.setNativeView(null); this.setNativeView(null);
this._androidView = null; this._androidView = null;
}
// this._iosView = null;
this._context = null;
} }
// this._iosView = null;
this._context = null;
if (this.domNode) { if (this.domNode) {
this.domNode.dispose(); this.domNode.dispose();
this.domNode = undefined; this.domNode = undefined;
@ -1099,6 +1123,7 @@ ViewBase.prototype._defaultPaddingBottom = 0;
ViewBase.prototype._defaultPaddingLeft = 0; ViewBase.prototype._defaultPaddingLeft = 0;
ViewBase.prototype._isViewBase = true; ViewBase.prototype._isViewBase = true;
ViewBase.prototype.recycleNativeView = 'never'; ViewBase.prototype.recycleNativeView = 'never';
ViewBase.prototype.reusable = false;
ViewBase.prototype._suspendNativeUpdatesCount = SuspendType.Loaded | SuspendType.NativeView | SuspendType.UISetup; ViewBase.prototype._suspendNativeUpdatesCount = SuspendType.Loaded | SuspendType.NativeView | SuspendType.UISetup;