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('6439', 'issues/issue-6439-page');
|
||||
examples.set('open-file-6895', 'issues/open-file-6895-page');
|
||||
examples.set("7469", "issues/issue-7469-page");
|
||||
|
||||
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 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.
|
||||
|
@ -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 */ = (<any>nativeView).defaultPaddings;
|
||||
if (result === undefined) {
|
||||
result = org.nativescript.widgets.ViewHelper.getPadding(nativeView);
|
||||
(<any>nativeView).defaultPaddings = result;
|
||||
}
|
||||
let result: any /* android.graphics.Rect */ = (<any>nativeView).defaultPaddings;
|
||||
if (result === undefined) {
|
||||
result = org.nativescript.widgets.ViewHelper.getPadding(nativeView);
|
||||
(<any>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;
|
||||
|
||||
|
Reference in New Issue
Block a user