Skip to content

Commit 2b64e17

Browse files
authored
fix(core): RootLayout view and shade cover should animate in parallel (NativeScript#10256)
1 parent 7aaa1d8 commit 2b64e17

File tree

2 files changed

+127
-89
lines changed

2 files changed

+127
-89
lines changed

packages/core/ui/layouts/root-layout/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class RootLayout extends GridLayout {
88
bringToFront(view: View, animated?: boolean): Promise<void>;
99
closeAll(): Promise<void[]>;
1010
getShadeCover(): View;
11-
openShadeCover(options: ShadeCoverOptions = {}): void;
11+
openShadeCover(options: ShadeCoverOptions = {}): Promise<void>;
1212
closeShadeCover(shadeCoverOptions: ShadeCoverOptions = {}): Promise<void>;
1313
}
1414

packages/core/ui/layouts/root-layout/root-layout-common.ts

Lines changed: 126 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,15 @@ export class RootLayoutBase extends GridLayout {
4040
return handled;
4141
}
4242

43-
// ability to add any view instance to composite views like layers
43+
/**
44+
* Ability to add any view instance to composite views like layers.
45+
*
46+
* @param view
47+
* @param options
48+
* @returns
49+
*/
4450
open(view: View, options: RootLayoutOptions = {}): Promise<void> {
45-
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
46-
47-
return new Promise<void>((resolve, reject) => {
51+
return new Promise((resolve, reject) => {
4852
if (!(view instanceof View)) {
4953
return reject(new Error(`Invalid open view: ${view}`));
5054
}
@@ -53,57 +57,68 @@ export class RootLayoutBase extends GridLayout {
5357
return reject(new Error(`${view} has already been added`));
5458
}
5559

56-
resolve();
57-
})
58-
.then(() => {
59-
// keep track of the views locally to be able to use their options later
60-
this.popupViews.push({ view: view, options: options });
61-
62-
if (options.shadeCover) {
63-
// perf optimization note: we only need 1 layer of shade cover
64-
// we just update properties if needed by additional overlaid views
65-
if (this.shadeCover) {
66-
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
67-
return this.updateShadeCover(this.shadeCover, options.shadeCover);
68-
}
69-
return this.openShadeCover(options.shadeCover);
60+
const toOpen = [];
61+
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
62+
63+
// keep track of the views locally to be able to use their options later
64+
this.popupViews.push({ view: view, options: options });
65+
66+
if (options.shadeCover) {
67+
// perf optimization note: we only need 1 layer of shade cover
68+
// we just update properties if needed by additional overlaid views
69+
if (this.shadeCover) {
70+
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
71+
toOpen.push(this.updateShadeCover(this.shadeCover, options.shadeCover));
72+
} else {
73+
toOpen.push(this.openShadeCover(options.shadeCover));
7074
}
71-
})
72-
.then(() => {
73-
view.opacity = 0; // always begin with view invisible when adding dynamically
74-
this.insertChild(view, this.getChildrenCount() + 1);
75+
}
7576

76-
return new Promise((resolve, reject) => {
77+
view.opacity = 0; // always begin with view invisible when adding dynamically
78+
this.insertChild(view, this.getChildrenCount() + 1);
79+
80+
toOpen.push(
81+
new Promise<void>((res, rej) => {
7782
setTimeout(() => {
7883
// only apply initial state and animate after the first tick - ensures safe areas and other measurements apply correctly
7984
this.applyInitialState(view, enterAnimationDefinition);
8085
this.getEnterAnimation(view, enterAnimationDefinition)
8186
.play()
82-
.then(() => {
83-
this.applyDefaultState(view);
84-
view.notify({ eventName: 'opened', object: view });
85-
resolve();
86-
})
87-
.catch((ex) => {
88-
reject(new Error(`Error playing enter animation: ${ex}`));
89-
});
87+
.then(
88+
() => {
89+
this.applyDefaultState(view);
90+
view.notify({ eventName: 'opened', object: view });
91+
res();
92+
},
93+
(err) => {
94+
rej(new Error(`Error playing enter animation: ${err}`));
95+
}
96+
);
9097
});
91-
});
92-
});
98+
})
99+
);
100+
101+
Promise.all(toOpen).then(
102+
() => {
103+
resolve();
104+
},
105+
(err) => {
106+
reject(err);
107+
}
108+
);
109+
});
93110
}
94111

95-
// optional animation parameter to overwrite close animation declared when opening popup
96-
// ability to remove any view instance from composite views
112+
/**
113+
* Ability to remove any view instance from composite views.
114+
* Optional animation parameter to overwrite close animation declared when opening popup.
115+
*
116+
* @param view
117+
* @param exitTo
118+
* @returns
119+
*/
97120
close(view: View, exitTo?: TransitionAnimation): Promise<void> {
98-
const cleanupAndFinish = () => {
99-
view.notify({ eventName: 'closed', object: view });
100-
this.removeChild(view);
101-
};
102-
103-
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
104-
let exitAnimationDefinition = exitTo;
105-
106-
return new Promise<void>((resolve, reject) => {
121+
return new Promise((resolve, reject) => {
107122
if (!(view instanceof View)) {
108123
return reject(new Error(`Invalid close view: ${view}`));
109124
}
@@ -112,42 +127,58 @@ export class RootLayoutBase extends GridLayout {
112127
return reject(new Error(`Unable to close popup. ${view} not found`));
113128
}
114129

115-
resolve();
116-
})
117-
.then(() => {
118-
const popupIndex = this.getPopupIndex(view);
119-
const poppedView = this.popupViews[popupIndex];
120-
121-
if (!exitAnimationDefinition) {
122-
exitAnimationDefinition = poppedView?.options?.animation?.exitTo;
123-
}
124-
125-
// Remove view from tracked popupviews
126-
this.popupViews.splice(popupIndex, 1);
130+
const toClose = [];
131+
const popupIndex = this.getPopupIndex(view);
132+
const poppedView = this.popupViews[popupIndex];
133+
const cleanupAndFinish = () => {
134+
view.notify({ eventName: 'closed', object: view });
135+
this.removeChild(view);
136+
resolve();
137+
};
138+
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
139+
const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;
140+
141+
// Remove view from tracked popupviews
142+
this.popupViews.splice(popupIndex, 1);
143+
144+
toClose.push(
145+
new Promise<void>((res, rej) => {
146+
if (exitAnimationDefinition) {
147+
this.getExitAnimation(view, exitAnimationDefinition)
148+
.play()
149+
.then(res, (err) => {
150+
rej(new Error(`Error playing exit animation: ${err}`));
151+
});
152+
} else {
153+
res();
154+
}
155+
})
156+
);
127157

128-
if (this.shadeCover) {
129-
// update shade cover with the topmost popupView options (if not specifically told to ignore)
158+
if (this.shadeCover) {
159+
// Update shade cover with the topmost popupView options (if not specifically told to ignore)
160+
if (this.popupViews.length) {
130161
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
131-
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
162+
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1].options?.shadeCover;
132163
if (shadeCoverOptions) {
133-
return this.updateShadeCover(this.shadeCover, shadeCoverOptions);
164+
toClose.push(this.updateShadeCover(this.shadeCover, shadeCoverOptions));
134165
}
135166
}
136-
// remove shade cover animation if this is the last opened popup view
137-
if (this.popupViews.length === 0) {
138-
return this.closeShadeCover(poppedView?.options?.shadeCover);
139-
}
167+
} else {
168+
// Remove shade cover animation if this is the last opened popup view
169+
toClose.push(this.closeShadeCover(poppedView?.options?.shadeCover));
140170
}
141-
})
142-
.then(() => {
143-
if (exitAnimationDefinition) {
144-
return this.getExitAnimation(view, exitAnimationDefinition)
145-
.play()
146-
.then(cleanupAndFinish.bind(this))
147-
.catch((ex) => Promise.reject(new Error(`Error playing exit animation: ${ex}`)));
171+
}
172+
173+
Promise.all(toClose).then(
174+
() => {
175+
cleanupAndFinish();
176+
},
177+
(err) => {
178+
reject(err);
148179
}
149-
cleanupAndFinish();
150-
});
180+
);
181+
});
151182
}
152183

153184
closeAll(): Promise<void[]> {
@@ -165,17 +196,28 @@ export class RootLayoutBase extends GridLayout {
165196
return this.shadeCover;
166197
}
167198

168-
openShadeCover(options: ShadeCoverOptions = {}) {
169-
if (this.shadeCover) {
170-
if (Trace.isEnabled()) {
171-
Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
199+
openShadeCover(options: ShadeCoverOptions = {}): Promise<void> {
200+
return new Promise((resolve) => {
201+
if (this.shadeCover) {
202+
if (Trace.isEnabled()) {
203+
Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
204+
}
205+
resolve();
206+
} else {
207+
// Create the one and only shade cover
208+
const shadeCover = this.createShadeCover();
209+
shadeCover.on('loaded', () => {
210+
this._initShadeCover(shadeCover, options);
211+
this.updateShadeCover(shadeCover, options).then(() => {
212+
resolve();
213+
});
214+
});
215+
216+
this.shadeCover = shadeCover;
217+
// Insert shade cover at index right above the first layout
218+
this.insertChild(this.shadeCover, this.staticChildCount + 1);
172219
}
173-
} else {
174-
// create the one and only shade cover
175-
this.shadeCover = this.createShadeCover(options);
176-
// insert shade cover at index right above the first layout
177-
this.insertChild(this.shadeCover, this.staticChildCount + 1);
178-
}
220+
});
179221
}
180222

181223
closeShadeCover(shadeCoverOptions: ShadeCoverOptions = {}): Promise<void> {
@@ -345,13 +387,9 @@ export class RootLayoutBase extends GridLayout {
345387
};
346388
}
347389

348-
private createShadeCover(shadeOptions: ShadeCoverOptions = {}): View {
390+
private createShadeCover(): View {
349391
const shadeCover = new GridLayout();
350392
shadeCover.verticalAlignment = 'bottom';
351-
shadeCover.on('loaded', () => {
352-
this._initShadeCover(shadeCover, shadeOptions);
353-
this.updateShadeCover(shadeCover, shadeOptions);
354-
});
355393
return shadeCover;
356394
}
357395

0 commit comments

Comments
 (0)