mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 02:54:11 +08:00
feat(core): reusable views (#9163)
This commit is contained in:

committed by
Nathan Walker

parent
e9e4934faf
commit
6cc130fa6f
15
apps/ui/src/issues/issue-7469-page.css
Normal file
15
apps/ui/src/issues/issue-7469-page.css
Normal 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;
|
||||||
|
}
|
158
apps/ui/src/issues/issue-7469-page.ts
Normal file
158
apps/ui/src/issues/issue-7469-page.ts
Normal 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();
|
||||||
|
}
|
37
apps/ui/src/issues/issue-7469-page.xml
Normal file
37
apps/ui/src/issues/issue-7469-page.xml
Normal 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>
|
@ -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;
|
||||||
}
|
}
|
||||||
|
13
packages/core/ui/core/view-base/index.d.ts
vendored
13
packages/core/ui/core/view-base/index.d.ts
vendored
@ -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.
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user