feat(core): flexibility using multiple RootLayouts (#10684)

This commit is contained in:
Dimitris-Rafail Katsampas
2025-02-02 19:42:31 +02:00
committed by GitHub
parent 79a0306f32
commit 4b87a35e51
7 changed files with 124 additions and 65 deletions

View File

@ -2,7 +2,7 @@ export { AbsoluteLayout } from './absolute-layout';
export { DockLayout } from './dock-layout';
export { FlexboxLayout } from './flexbox-layout';
export { GridLayout, GridUnitType, ItemSpec } from './grid-layout';
export { RootLayout, getRootLayout, RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { RootLayout, getRootLayout, getRootLayoutById, RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';

View File

@ -2,7 +2,7 @@ export { AbsoluteLayout } from './absolute-layout';
export { DockLayout } from './dock-layout';
export { FlexboxLayout } from './flexbox-layout';
export { GridLayout, GridUnitType, ItemSpec } from './grid-layout';
export { RootLayout, getRootLayout } from './root-layout';
export { RootLayout, getRootLayout, getRootLayoutById } from './root-layout';
export type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';

View File

@ -8,10 +8,6 @@ import { LinearGradient } from '../../styling/linear-gradient';
export * from './root-layout-common';
export class RootLayout extends RootLayoutBase {
constructor() {
super();
}
insertChild(view: View, atIndex: number): void {
super.insertChild(view, atIndex);
if (!view.hasGestureObservers()) {

View File

@ -16,6 +16,7 @@ export class RootLayout extends GridLayout {
}
export function getRootLayout(): RootLayout;
export function getRootLayoutById(id: string): RootLayout;
export interface RootLayoutOptions {
shadeCover?: ShadeCoverOptions;

View File

@ -8,16 +8,19 @@ import { parseLinearGradient } from '../../../css/parser';
export * from './root-layout-common';
export class RootLayout extends RootLayoutBase {
nativeViewProtected: UIView;
// perf optimization: only create and insert gradients if settings change
private _currentGradient: string;
private _gradientLayer: CAGradientLayer;
constructor() {
super();
public disposeNativeView(): void {
super.disposeNativeView();
this._cleanupPlatformShadeCover();
}
protected _bringToFront(view: View) {
(<UIView>this.nativeViewProtected).bringSubviewToFront(view.nativeViewProtected);
this.nativeViewProtected.bringSubviewToFront(view.nativeViewProtected);
}
protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
@ -46,7 +49,11 @@ export class RootLayout extends RootLayoutBase {
iosViewUtils.drawGradient(view.nativeViewProtected, this._gradientLayer, LinearGradient.parse(parsedGradient.value));
view.nativeViewProtected.layer.insertSublayerAtIndex(this._gradientLayer, 0);
}
} else {
// Dispose gradient if new color is null or a plain color
this._cleanupPlatformShadeCover();
}
UIView.animateWithDurationAnimationsCompletion(
duration,
() => {
@ -66,7 +73,7 @@ export class RootLayout extends RootLayoutBase {
},
(completed: boolean) => {
resolve();
}
},
);
}
});
@ -87,7 +94,7 @@ export class RootLayout extends RootLayoutBase {
},
(completed: boolean) => {
resolve();
}
},
);
}
});
@ -95,6 +102,7 @@ export class RootLayout extends RootLayoutBase {
protected _cleanupPlatformShadeCover(): void {
this._currentGradient = null;
if (this._gradientLayer != null) {
this._gradientLayer.removeFromSuperlayer();
this._gradientLayer = null;

View File

@ -6,30 +6,29 @@ import { RootLayout, RootLayoutOptions, ShadeCoverOptions, TransitionAnimation }
import { Animation } from '../../animation';
import { AnimationDefinition } from '../../animation';
import { isNumber } from '../../../utils/types';
import { _findRootLayoutById, _pushIntoRootLayoutStack, _removeFromRootLayoutStack, _geRootLayoutFromStack } from './root-layout-stack';
@CSSType('RootLayout')
export class RootLayoutBase extends GridLayout {
private shadeCover: View;
private staticChildCount: number;
private popupViews: { view: View; options: RootLayoutOptions }[] = [];
private _shadeCover: View;
private _popupViews: { view: View; options: RootLayoutOptions }[] = [];
constructor() {
super();
global.rootLayout = this;
public initNativeView(): void {
super.initNativeView();
_pushIntoRootLayoutStack(this);
}
public onLoaded() {
// get actual content count of rootLayout (elements between the <RootLayout> tags in the template).
// All popups will be inserted dynamically at a higher index
this.staticChildCount = this.getChildrenCount();
public disposeNativeView(): void {
super.disposeNativeView();
super.onLoaded();
_removeFromRootLayoutStack(this);
}
public _onLivesync(context?: ModuleContext): boolean {
let handled = false;
if (this.popupViews.length > 0) {
if (this._popupViews.length > 0) {
this.closeAll();
handled = true;
}
@ -55,29 +54,32 @@ export class RootLayoutBase extends GridLayout {
}
if (this.hasChild(view)) {
return reject(new Error(`${view} has already been added`));
return reject(new Error(`View ${view} has already been added to the root layout`));
}
const toOpen = [];
const enterAnimationDefinition = options.animation ? options.animation.enterFrom : null;
// keep track of the views locally to be able to use their options later
this.popupViews.push({ view: view, options: options });
// Keep track of the views locally to be able to use their options later
this._popupViews.push({ view: view, options: options });
// Always begin with view invisible when adding dynamically
view.opacity = 0;
// Add view to view tree before adding shade cover
// Before being added to view tree, shade cover calculates the index to be inserted based on existing popup views
this.insertChild(view, this.getChildrenCount());
if (options.shadeCover) {
// perf optimization note: we only need 1 layer of shade cover
// we just update properties if needed by additional overlaid views
if (this.shadeCover) {
if (this._shadeCover) {
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
toOpen.push(this.updateShadeCover(this.shadeCover, options.shadeCover));
toOpen.push(this.updateShadeCover(this._shadeCover, options.shadeCover));
} else {
toOpen.push(this.openShadeCover(options.shadeCover));
}
}
view.opacity = 0; // always begin with view invisible when adding dynamically
this.insertChild(view, this.getChildrenCount() + 1);
toOpen.push(
new Promise<void>((res, rej) => {
setTimeout(() => {
@ -125,12 +127,12 @@ export class RootLayoutBase extends GridLayout {
}
if (!this.hasChild(view)) {
return reject(new Error(`Unable to close popup. ${view} not found`));
return reject(new Error(`Unable to close popup. View ${view} not found`));
}
const toClose = [];
const popupIndex = this.getPopupIndex(view);
const poppedView = this.popupViews[popupIndex];
const poppedView = this._popupViews[popupIndex];
const cleanupAndFinish = () => {
view.notify({ eventName: 'closed', object: view });
this.removeChild(view);
@ -141,7 +143,7 @@ export class RootLayoutBase extends GridLayout {
// Remove view from tracked popupviews
if (popupIndex > -1) {
this.popupViews.splice(popupIndex, 1);
this._popupViews.splice(popupIndex, 1);
}
toClose.push(
@ -158,13 +160,13 @@ export class RootLayoutBase extends GridLayout {
}),
);
if (this.shadeCover) {
if (this._shadeCover) {
// Update shade cover with the topmost popupView options (if not specifically told to ignore)
if (this.popupViews.length) {
if (this._popupViews.length) {
if (!poppedView?.options?.shadeCover?.ignoreShadeRestore) {
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1].options?.shadeCover;
const shadeCoverOptions = this._popupViews[this._popupViews.length - 1].options?.shadeCover;
if (shadeCoverOptions) {
toClose.push(this.updateShadeCover(this.shadeCover, shadeCoverOptions));
toClose.push(this.updateShadeCover(this._shadeCover, shadeCoverOptions));
}
}
} else {
@ -186,7 +188,7 @@ export class RootLayoutBase extends GridLayout {
closeAll(): Promise<void[]> {
const toClose = [];
const views = this.popupViews.map((popupView) => popupView.view);
const views = this._popupViews.map((popupView) => popupView.view);
// Close all views at the same time and wait for all of them
for (const view of views) {
@ -196,12 +198,25 @@ export class RootLayoutBase extends GridLayout {
}
getShadeCover(): View {
return this.shadeCover;
return this._shadeCover;
}
openShadeCover(options: ShadeCoverOptions = {}): Promise<void> {
return new Promise((resolve) => {
if (this.shadeCover) {
const childrenCount = this.getChildrenCount();
let indexToAdd: number;
if (this._popupViews.length) {
const { view } = this._popupViews[0];
const index = this.getChildIndex(view);
indexToAdd = index > -1 ? index : childrenCount;
} else {
indexToAdd = childrenCount;
}
if (this._shadeCover) {
if (Trace.isEnabled()) {
Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
}
@ -216,9 +231,9 @@ export class RootLayoutBase extends GridLayout {
});
});
this.shadeCover = shadeCover;
// Insert shade cover at index right above the first layout
this.insertChild(this.shadeCover, this.staticChildCount + 1);
this._shadeCover = shadeCover;
// Insert shade cover at index right below the first popup view
this.insertChild(this._shadeCover, indexToAdd);
}
});
}
@ -226,15 +241,15 @@ export class RootLayoutBase extends GridLayout {
closeShadeCover(shadeCoverOptions: ShadeCoverOptions = {}): Promise<void> {
return new Promise((resolve) => {
// if shade cover is displayed and the last popup is closed, also close the shade cover
if (this.shadeCover) {
return this._closeShadeCover(this.shadeCover, shadeCoverOptions).then(() => {
if (this.shadeCover) {
this.shadeCover.off('loaded');
if (this.shadeCover.parent) {
this.removeChild(this.shadeCover);
if (this._shadeCover) {
return this._closeShadeCover(this._shadeCover, shadeCoverOptions).then(() => {
if (this._shadeCover) {
this._shadeCover.off('loaded');
if (this._shadeCover.parent) {
this.removeChild(this._shadeCover);
}
}
this.shadeCover = null;
this._shadeCover = null;
// cleanup any platform specific details related to shade cover
this._cleanupPlatformShadeCover();
resolve();
@ -245,10 +260,16 @@ export class RootLayoutBase extends GridLayout {
}
topmost(): View {
return this.popupViews.length ? this.popupViews[this.popupViews.length - 1].view : null;
return this._popupViews.length ? this._popupViews[this._popupViews.length - 1].view : null;
}
// bring any view instance open on the rootlayout to front of all the children visually
/**
* This method causes the requested view to overlap its siblings by bring it to front.
*
* @param view
* @param animated
* @returns
*/
bringToFront(view: View, animated: boolean = false): Promise<void> {
return new Promise((resolve, reject) => {
if (!(view instanceof View)) {
@ -256,19 +277,23 @@ export class RootLayoutBase extends GridLayout {
}
if (!this.hasChild(view)) {
return reject(new Error(`${view} not found or already at topmost`));
return reject(new Error(`View ${view} is not a child of the root layout`));
}
const popupIndex = this.getPopupIndex(view);
// popupview should be present and not already the topmost view
if (popupIndex < 0 || popupIndex == this.popupViews.length - 1) {
return reject(new Error(`${view} not found or already at topmost`));
if (popupIndex < 0) {
return reject(new Error(`View ${view} is not a child of the root layout`));
}
if (popupIndex == this._popupViews.length - 1) {
return reject(new Error(`View ${view} is already the topmost view in the rootlayout`));
}
// keep the popupViews array in sync with the stacking of the views
const currentView = this.popupViews[popupIndex];
this.popupViews.splice(popupIndex, 1);
this.popupViews.push(currentView);
const currentView = this._popupViews[popupIndex];
this._popupViews.splice(popupIndex, 1);
this._popupViews.push(currentView);
const exitAnimation = this.getViewExitState(view);
if (animated && exitAnimation) {
@ -302,14 +327,14 @@ export class RootLayoutBase extends GridLayout {
// update shadeCover to reflect topmost's shadeCover options
const shadeCoverOptions = currentView?.options?.shadeCover;
if (shadeCoverOptions) {
this.updateShadeCover(this.shadeCover, shadeCoverOptions);
this.updateShadeCover(this._shadeCover, shadeCoverOptions);
}
resolve();
});
}
private getPopupIndex(view: View): number {
return this.popupViews.findIndex((popupView) => popupView.view === view);
return this._popupViews.findIndex((popupView) => popupView.view === view);
}
private getViewInitialState(view: View): TransitionAnimation {
@ -317,7 +342,7 @@ export class RootLayoutBase extends GridLayout {
if (popupIndex === -1) {
return;
}
const initialState = this.popupViews[popupIndex]?.options?.animation?.enterFrom;
const initialState = this._popupViews[popupIndex]?.options?.animation?.enterFrom;
if (!initialState) {
return;
}
@ -329,7 +354,7 @@ export class RootLayoutBase extends GridLayout {
if (popupIndex === -1) {
return;
}
const exitAnimation = this.popupViews[popupIndex]?.options?.animation?.exitTo;
const exitAnimation = this._popupViews[popupIndex]?.options?.animation?.exitTo;
if (!exitAnimation) {
return;
}
@ -428,7 +453,11 @@ export class RootLayoutBase extends GridLayout {
}
export function getRootLayout(): RootLayout {
return <RootLayout>global.rootLayout;
return _geRootLayoutFromStack(0);
}
export function getRootLayoutById(id: string): RootLayout {
return _findRootLayoutById(id);
}
export const defaultTransitionAnimation: TransitionAnimation = {

View File

@ -0,0 +1,25 @@
import { RootLayoutBase } from './root-layout-common';
const rootLayoutStack: RootLayoutBase[] = [];
export function _findRootLayoutById(id: string): RootLayoutBase {
return rootLayoutStack.find((rootLayout) => rootLayout.id && rootLayout.id === id);
}
export function _pushIntoRootLayoutStack(rootLayout: RootLayoutBase): void {
if (!rootLayoutStack.includes(rootLayout)) {
rootLayoutStack.push(rootLayout);
}
}
export function _removeFromRootLayoutStack(rootLayout: RootLayoutBase): void {
const index = rootLayoutStack.indexOf(rootLayout);
if (index > -1) {
rootLayoutStack.splice(index, 1);
}
}
export function _geRootLayoutFromStack(index: number): RootLayoutBase {
return rootLayoutStack.length > index ? rootLayoutStack[index] : null;
}