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 { DockLayout } from './dock-layout';
export { FlexboxLayout } from './flexbox-layout'; export { FlexboxLayout } from './flexbox-layout';
export { GridLayout, GridUnitType, ItemSpec } from './grid-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 { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout'; export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base'; export { LayoutBase } from './layout-base';

View File

@ -2,7 +2,7 @@ export { AbsoluteLayout } from './absolute-layout';
export { DockLayout } from './dock-layout'; export { DockLayout } from './dock-layout';
export { FlexboxLayout } from './flexbox-layout'; export { FlexboxLayout } from './flexbox-layout';
export { GridLayout, GridUnitType, ItemSpec } from './grid-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 type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout'; export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout'; export { WrapLayout } from './wrap-layout';

View File

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

View File

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

View File

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

View File

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