diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml
index 8d351e81c7..71af635418 100644
--- a/apps/toolbox/src/main-page.xml
+++ b/apps/toolbox/src/main-page.xml
@@ -3,49 +3,84 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
-
- -->
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+ -->
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
diff --git a/apps/toolbox/src/main-view-model.ts b/apps/toolbox/src/main-view-model.ts
index f69e5f9850..c7dc150e37 100644
--- a/apps/toolbox/src/main-view-model.ts
+++ b/apps/toolbox/src/main-view-model.ts
@@ -1,4 +1,5 @@
-import { Observable, Frame, StackLayout } from '@nativescript/core';
+import { Observable, Frame, View, StackLayout, getRootLayout, EventData, RootLayout, RootLayoutOptions } from '@nativescript/core';
+import { AnimationCurve } from '@nativescript/core/ui/enums';
export class HelloWorldModel extends Observable {
private _counter: number;
@@ -37,6 +38,123 @@ export class HelloWorldModel extends Observable {
this.updateMessage();
}
+ popupViews: { view: View; options: RootLayoutOptions; extra?: any }[] = [
+ {
+ view: this.getPopup('#EA5936', 110, -30),
+ options: {
+ shadeCover: {
+ color: '#FFF',
+ opacity: 0.7,
+ tapToClose: true,
+ },
+ animation: {
+ enterFrom: {
+ opacity: 0,
+ translateY: 500,
+ duration: 500,
+ },
+ exitTo: {
+ opacity: 0,
+ duration: 300,
+ },
+ },
+ },
+ extra: {
+ customExitAnimation: {
+ opacity: 0,
+ translate: { x: 0, y: -500 },
+ },
+ },
+ },
+ {
+ view: this.getPopup('#232652', 110, 0),
+ options: {
+ shadeCover: {
+ color: 'pink',
+ opacity: 0.7,
+ tapToClose: false,
+ animation: {
+ exitTo: {
+ scaleX: 0,
+ },
+ },
+ },
+ },
+ },
+ {
+ view: this.getPopup('#E1E4E8', 110, 30),
+ options: {
+ shadeCover: {
+ color: '#ffffdd',
+ opacity: 0.5,
+ tapToClose: true,
+ ignoreShadeRestore: true,
+ animation: {
+ enterFrom: {
+ translateX: -1000,
+ duration: 500,
+ },
+ exitTo: {
+ rotate: -180,
+ duration: 500,
+ },
+ },
+ },
+ animation: {
+ enterFrom: {
+ rotate: 180,
+ duration: 300,
+ },
+ exitTo: {
+ rotate: 180,
+ opacity: 0,
+ duration: 300,
+ curve: AnimationCurve.spring,
+ },
+ },
+ },
+ },
+ ];
+
+ open(args: EventData): void {
+ getRootLayout()
+ .open(this.popupViews[(args.object).popupIndex].view, this.popupViews[(args.object).popupIndex].options)
+ .then(() => console.log('opened'))
+ .catch((ex) => console.error(ex));
+ }
+
+ bringToFront(args: EventData): void {
+ getRootLayout()
+ .bringToFront(this.popupViews[(args.object).popupIndex].view, true)
+ .then(() => console.log('brought to front'))
+ .catch((ex) => console.error(ex));
+ }
+
+ close(args: EventData): void {
+ if (this.popupViews[(args.object).popupIndex]?.extra?.customExitAnimation) {
+ getRootLayout()
+ .close(this.popupViews[(args.object).popupIndex].view, this.popupViews[(args.object).popupIndex].extra.customExitAnimation)
+ .then(() => console.log('closed with custom exit animation'))
+ .catch((ex) => console.error(ex));
+ } else {
+ getRootLayout()
+ .close(this.popupViews[(args.object).popupIndex].view)
+ .then(() => console.log('closed'))
+ .catch((ex) => console.error(ex));
+ }
+ }
+
+ getPopup(color: string, size: number, offset: number): View {
+ const layout = new StackLayout();
+ layout.height = size;
+ layout.width = size;
+ layout.marginTop = offset;
+ layout.marginLeft = offset;
+ layout.backgroundColor = color;
+ layout.borderRadius = 10;
+ return layout;
+ }
+
viewList() {
Frame.topmost().navigate({
moduleName: 'list-page',
diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts
index 6703d76633..ea7b445446 100644
--- a/packages/core/global-types.d.ts
+++ b/packages/core/global-types.d.ts
@@ -126,6 +126,9 @@ declare namespace NodeJS {
isIOS?: boolean;
isAndroid?: boolean;
__requireOverride?: (name: string, dir: string) => any;
+
+ // used to get the rootlayout instance to add/remove childviews
+ rootLayout: any;
}
}
diff --git a/packages/core/ui/layouts/index.d.ts b/packages/core/ui/layouts/index.d.ts
index 62c6bd6013..aefba214fa 100644
--- a/packages/core/ui/layouts/index.d.ts
+++ b/packages/core/ui/layouts/index.d.ts
@@ -2,6 +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 { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';
diff --git a/packages/core/ui/layouts/index.ts b/packages/core/ui/layouts/index.ts
index 62c6bd6013..1150ae1e60 100644
--- a/packages/core/ui/layouts/index.ts
+++ b/packages/core/ui/layouts/index.ts
@@ -2,6 +2,8 @@ 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 type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
export { StackLayout } from './stack-layout';
export { WrapLayout } from './wrap-layout';
export { LayoutBase } from './layout-base';
diff --git a/packages/core/ui/layouts/root-layout/index.android.ts b/packages/core/ui/layouts/root-layout/index.android.ts
new file mode 100644
index 0000000000..c810e01666
--- /dev/null
+++ b/packages/core/ui/layouts/root-layout/index.android.ts
@@ -0,0 +1,102 @@
+import { Color } from '../../../color';
+import { View } from '../../core/view';
+import { RootLayoutBase, defaultShadeCoverOptions } from './root-layout-common';
+import { TransitionAnimation, ShadeCoverOptions } from '.';
+
+export * from './root-layout-common';
+
+export class RootLayout extends RootLayoutBase {
+ constructor() {
+ super();
+ }
+
+ protected _bringToFront(view: View) {
+ (view.nativeViewProtected).bringToFront();
+ }
+
+ protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
+ const initialState = {
+ ...defaultShadeCoverOptions.animation.enterFrom,
+ ...shadeOptions?.animation?.enterFrom,
+ };
+ this._playAnimation(this._getAnimationSet(view, initialState));
+ }
+
+ protected _updateShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise {
+ const options = {
+ ...defaultShadeCoverOptions,
+ ...shadeOptions,
+ };
+ const duration = options.animation?.enterFrom?.duration || defaultShadeCoverOptions.animation.enterFrom.duration;
+ return this._playAnimation(
+ this._getAnimationSet(
+ view,
+ {
+ translateX: 0,
+ translateY: 0,
+ scaleX: 1,
+ scaleY: 1,
+ rotate: 0,
+ opacity: options.opacity,
+ },
+ options.color
+ ),
+ duration
+ );
+ }
+
+ protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise {
+ const exitState = {
+ ...defaultShadeCoverOptions.animation.exitTo,
+ ...shadeOptions?.animation?.exitTo,
+ };
+ return this._playAnimation(this._getAnimationSet(view, exitState), exitState?.duration);
+ }
+
+ private _getAnimationSet(view: View, shadeCoverAnimation: TransitionAnimation, backgroundColor: string = defaultShadeCoverOptions.color): Array {
+ const animationSet = Array.create(android.animation.Animator, 7);
+ animationSet[0] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'translationX', [shadeCoverAnimation.translateX]);
+ animationSet[1] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'translationY', [shadeCoverAnimation.translateY]);
+ animationSet[2] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'scaleX', [shadeCoverAnimation.scaleX]);
+ animationSet[3] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'scaleY', [shadeCoverAnimation.scaleY]);
+ animationSet[4] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'rotation', [shadeCoverAnimation.rotate]);
+ animationSet[5] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'alpha', [shadeCoverAnimation.opacity]);
+ animationSet[6] = this._getBackgroundColorAnimator(view, backgroundColor);
+ return animationSet;
+ }
+
+ private _getBackgroundColorAnimator(view: View, backgroundColor: string): android.animation.ValueAnimator {
+ const nativeArray = Array.create(java.lang.Object, 2);
+ nativeArray[0] = view.backgroundColor ? java.lang.Integer.valueOf((view.backgroundColor).argb) : java.lang.Integer.valueOf(-1);
+ nativeArray[1] = java.lang.Integer.valueOf(new Color(backgroundColor).argb);
+ const backgroundColorAnimator = android.animation.ValueAnimator.ofObject(new android.animation.ArgbEvaluator(), nativeArray);
+ backgroundColorAnimator.addUpdateListener(
+ new android.animation.ValueAnimator.AnimatorUpdateListener({
+ onAnimationUpdate(animator: android.animation.ValueAnimator) {
+ let argb = (animator.getAnimatedValue()).intValue();
+ view.backgroundColor = new Color(argb);
+ },
+ })
+ );
+ return backgroundColorAnimator;
+ }
+
+ private _playAnimation(animationSet: Array, duration: number = 0): Promise {
+ return new Promise((resolve) => {
+ const animatorSet = new android.animation.AnimatorSet();
+ animatorSet.playTogether(animationSet);
+ animatorSet.setDuration(duration);
+ animatorSet.addListener(
+ new android.animation.Animator.AnimatorListener({
+ onAnimationStart: function (animator: android.animation.Animator): void {},
+ onAnimationEnd: function (animator: android.animation.Animator): void {
+ resolve();
+ },
+ onAnimationRepeat: function (animator: android.animation.Animator): void {},
+ onAnimationCancel: function (animator: android.animation.Animator): void {},
+ })
+ );
+ animatorSet.start();
+ });
+ }
+}
diff --git a/packages/core/ui/layouts/root-layout/index.d.ts b/packages/core/ui/layouts/root-layout/index.d.ts
new file mode 100644
index 0000000000..174474df5b
--- /dev/null
+++ b/packages/core/ui/layouts/root-layout/index.d.ts
@@ -0,0 +1,43 @@
+import { GridLayout } from '../grid-layout';
+import { View } from '../../core/view';
+import { AnimationCurve } from '../../enums';
+
+export class RootLayout extends GridLayout {
+ open(view: View, options?: RootLayoutOptions): Promise;
+ close(view: View, exitTo?: TransitionAnimation): Promise;
+ bringToFront(view: View, animated?: boolean): Promise;
+ closeAll(): Promise;
+ getShadeCover(): View;
+}
+
+export function getRootLayout(): RootLayout;
+
+export interface RootLayoutOptions {
+ shadeCover?: ShadeCoverOptions;
+ animation?: {
+ enterFrom?: TransitionAnimation;
+ exitTo?: TransitionAnimation;
+ };
+}
+
+export interface ShadeCoverOptions {
+ opacity?: number;
+ color?: string;
+ tapToClose?: boolean;
+ animation?: {
+ enterFrom?: TransitionAnimation; // these will only be applied if its the first one to be opened
+ exitTo?: TransitionAnimation; // these will only be applied if its the last one to be closed
+ };
+ ignoreShadeRestore?: boolean;
+}
+
+export interface TransitionAnimation {
+ translateX?: number;
+ translateY?: number;
+ scaleX?: number;
+ scaleY?: number;
+ rotate?: number; // in degrees
+ opacity?: number;
+ duration?: number; // in milliseconds
+ curve?: AnimationCurve;
+}
diff --git a/packages/core/ui/layouts/root-layout/index.ios.ts b/packages/core/ui/layouts/root-layout/index.ios.ts
new file mode 100644
index 0000000000..bce344bd85
--- /dev/null
+++ b/packages/core/ui/layouts/root-layout/index.ios.ts
@@ -0,0 +1,87 @@
+import { Color } from '../../../color';
+import { View } from '../../core/view';
+import { RootLayoutBase, defaultShadeCoverOptions } from './root-layout-common';
+import { TransitionAnimation, ShadeCoverOptions } from '.';
+export * from './root-layout-common';
+
+export class RootLayout extends RootLayoutBase {
+ constructor() {
+ super();
+ }
+
+ protected _bringToFront(view: View) {
+ (this.nativeViewProtected).bringSubviewToFront(view.nativeViewProtected);
+ }
+
+ protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
+ const initialState = {
+ ...defaultShadeCoverOptions.animation.enterFrom,
+ ...shadeOptions?.animation?.enterFrom,
+ };
+ this._applyAnimationProperties(view, initialState);
+ }
+
+ protected _updateShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise {
+ return new Promise((resolve) => {
+ const options = {
+ ...defaultShadeCoverOptions,
+ ...shadeOptions,
+ };
+ if (view && view.nativeViewProtected) {
+ const duration = this._convertDurationToSeconds(options.animation?.enterFrom?.duration || defaultShadeCoverOptions.animation.enterFrom.duration);
+ UIView.animateWithDurationAnimationsCompletion(
+ duration,
+ () => {
+ view.nativeViewProtected.backgroundColor = new Color(options.color).ios;
+ this._applyAnimationProperties(view, {
+ translateX: 0,
+ translateY: 0,
+ scaleX: 1,
+ scaleY: 1,
+ rotate: 0,
+ opacity: shadeOptions.opacity,
+ });
+ },
+ (completed: boolean) => {
+ resolve();
+ }
+ );
+ }
+ });
+ }
+
+ protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise {
+ return new Promise((resolve) => {
+ const exitState = {
+ ...defaultShadeCoverOptions.animation.exitTo,
+ ...shadeOptions?.animation?.exitTo,
+ };
+
+ if (view && view.nativeViewProtected) {
+ UIView.animateWithDurationAnimationsCompletion(
+ this._convertDurationToSeconds(exitState.duration),
+ () => {
+ this._applyAnimationProperties(view, exitState);
+ },
+ (completed: boolean) => {
+ resolve();
+ }
+ );
+ }
+ });
+ }
+
+ private _applyAnimationProperties(view: View, shadeCoverAnimation: TransitionAnimation): void {
+ const translate = CGAffineTransformMakeTranslation(shadeCoverAnimation.translateX, shadeCoverAnimation.translateY);
+ // ios doesn't like scale being 0, default it to a small number greater than 0
+ const scale = CGAffineTransformMakeScale(shadeCoverAnimation.scaleX || 0.1, shadeCoverAnimation.scaleY || 0.1);
+ const rotate = CGAffineTransformMakeRotation((shadeCoverAnimation.rotate * Math.PI) / 180); // convert degress to radians
+ const translateAndScale = CGAffineTransformConcat(translate, scale);
+ view.nativeViewProtected.transform = CGAffineTransformConcat(rotate, translateAndScale);
+ view.nativeViewProtected.alpha = shadeCoverAnimation.opacity;
+ }
+
+ private _convertDurationToSeconds(duration: number): number {
+ return duration / 1000;
+ }
+}
diff --git a/packages/core/ui/layouts/root-layout/root-layout-common.ts b/packages/core/ui/layouts/root-layout/root-layout-common.ts
new file mode 100644
index 0000000000..84a0ab889b
--- /dev/null
+++ b/packages/core/ui/layouts/root-layout/root-layout-common.ts
@@ -0,0 +1,389 @@
+import { AnimationCurve } from '../../enums';
+import { Trace } from '../../../trace';
+import { CSSType, View } from '../../core/view';
+import { GridLayout } from '../grid-layout';
+import { RootLayout, RootLayoutOptions, ShadeCoverOptions, TransitionAnimation } from '.';
+import { Animation } from '../../animation';
+
+@CSSType('RootLayout')
+export class RootLayoutBase extends GridLayout {
+ private shadeCover: View;
+ private staticChildCount: number;
+ private popupViews: { view: View; options: RootLayoutOptions }[] = [];
+
+ constructor() {
+ super();
+ global.rootLayout = this;
+ this.on('loaded', () => {
+ // get actual content count of rootLayout (elements between the tags in the template).
+ // All popups will be inserted dynamically at a higher index
+ this.staticChildCount = this.getChildrenCount();
+ });
+ }
+
+ // ability to add any view instance to compositie views like layers
+ open(view: View, options?: RootLayoutOptions): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ if (this.hasChild(view)) {
+ if (Trace.isEnabled()) {
+ Trace.write(`${view} has already been added`, Trace.categories.Layout);
+ }
+ } else {
+ // keep track of the views locally to be able to use their options later
+ this.popupViews.push({ view: view, options: options });
+
+ // only insert 1 layer of shade cover (don't insert another one if already present)
+ if (options?.shadeCover && !this.shadeCover) {
+ this.shadeCover = this.createShadeCover(options.shadeCover);
+ // insert shade cover at index right above the first layout
+ this.insertChild(this.shadeCover, this.staticChildCount + 1);
+ }
+
+ // overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
+ else if (options?.shadeCover && this.shadeCover) {
+ this.updateShadeCover(this.shadeCover, options.shadeCover);
+ }
+
+ this.insertChild(view, this.getChildrenCount() + 1);
+
+ if (options?.animation?.enterFrom) {
+ this.applyInitialState(view, options.animation.enterFrom);
+ this.getEnterAnimation(view, options.animation.enterFrom)
+ .play()
+ .then(() => {
+ this.applyDefaultState(view);
+ resolve();
+ })
+ .catch((ex) => {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error playing enter animation: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ });
+ } else {
+ resolve();
+ }
+ }
+ } catch (ex) {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error opening popup (${view}): ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ }
+ });
+ }
+
+ // optional animation parameter to overwrite close animation declared when opening popup
+ // ability to remove any view instance from composite views
+ close(view: View, exitTo?: TransitionAnimation): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.hasChild(view)) {
+ try {
+ const popupIndex = this.getPopupIndex(view);
+ // use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
+ const exitAnimationDefinition = exitTo || this.popupViews[popupIndex]?.options?.animation?.exitTo;
+
+ // Remove view from local array
+ const poppedView = this.popupViews[popupIndex];
+ this.popupViews.splice(popupIndex, 1);
+
+ // update shade cover with the topmost popupView options (if not specifically told to ignore)
+ const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
+ if (shadeCoverOptions && !poppedView?.options?.shadeCover.ignoreShadeRestore) {
+ this.updateShadeCover(this.shadeCover, shadeCoverOptions);
+ }
+
+ if (exitAnimationDefinition) {
+ const exitAnimation = this.getExitAnimation(view, exitAnimationDefinition);
+ const exitAnimations: Promise[] = [exitAnimation.play()];
+
+ // add remove shade cover animation if this is the last opened popup view
+ if (this.popupViews.length === 0) {
+ exitAnimations.push(this.closeShadeCover(poppedView.options.shadeCover));
+ }
+ return Promise.all(exitAnimations)
+ .then(() => {
+ this.removeChild(view);
+ resolve();
+ })
+ .catch((ex) => {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error playing exit animation: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ });
+ }
+ this.removeChild(view);
+
+ // also remove shade cover if this is the last opened popup view
+ if (this.popupViews.length === 0) {
+ this.closeShadeCover(poppedView.options.shadeCover);
+ }
+ resolve();
+ } catch (ex) {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error closing popup (${view}): ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ }
+ } else {
+ if (Trace.isEnabled()) {
+ Trace.write(`Unable to close popup. ${view} not found`, Trace.categories.Layout);
+ }
+ }
+ });
+ }
+
+ closeAll(): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ while (this.popupViews.length > 0) {
+ // remove all children in the popupViews array
+ this.close(this.popupViews[this.popupViews.length - 1].view);
+ }
+ resolve();
+ } catch (ex) {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error closing popups: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ }
+ });
+ }
+
+ // bring any view instance open on the rootlayout to front of all the children visually
+ bringToFront(view: View, animated: boolean = false): Promise {
+ return new Promise((resolve, reject) => {
+ try {
+ const popupIndex = this.getPopupIndex(view);
+ // popupview should be present and not already the topmost view
+ if (popupIndex > -1 && popupIndex !== this.popupViews.length - 1) {
+ // keep the popupViews array in sync with the stacking of the views
+ const currentView = this.popupViews[this.getPopupIndex(view)];
+ this.popupViews.splice(this.getPopupIndex(view), 1);
+ this.popupViews.push(currentView);
+
+ if (this.hasChild(view)) {
+ const exitAnimation = this.getViewExitState(view);
+ if (animated && exitAnimation) {
+ this.getExitAnimation(view, exitAnimation)
+ .play()
+ .then(() => {
+ this._bringToFront(view);
+ const initialState = this.getViewInitialState(currentView.view);
+ if (initialState) {
+ this.applyInitialState(view, initialState);
+ this.getEnterAnimation(view, initialState)
+ .play()
+ .then(() => {
+ this.applyDefaultState(view);
+ })
+ .catch((ex) => {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error playing enter animation: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ });
+ } else {
+ this.applyDefaultState(view);
+ }
+ })
+ .catch((ex) => {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error playing exit animation: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ this._bringToFront(view);
+ });
+ } else {
+ this._bringToFront(view);
+ }
+ }
+
+ // update shadeCover to reflect topmost's shadeCover options
+ const shadeCoverOptions = currentView?.options?.shadeCover;
+ if (shadeCoverOptions) {
+ this.updateShadeCover(this.shadeCover, shadeCoverOptions);
+ }
+
+ resolve();
+ } else {
+ if (Trace.isEnabled()) {
+ Trace.write(`${view} not found or already at topmost`, Trace.categories.Layout);
+ }
+ }
+ } catch (ex) {
+ if (Trace.isEnabled()) {
+ Trace.write(`Error in bringing view to front: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
+ }
+ }
+ });
+ }
+
+ getShadeCover(): View {
+ return this.shadeCover;
+ }
+
+ private getPopupIndex(view: View): number {
+ return this.popupViews.findIndex((popupView) => popupView.view === view);
+ }
+
+ private getViewInitialState(view: View): TransitionAnimation {
+ const popupIndex = this.getPopupIndex(view);
+ if (popupIndex === -1) {
+ return;
+ }
+ const initialState = this.popupViews[popupIndex]?.options?.animation?.enterFrom;
+ if (!initialState) {
+ return;
+ }
+ return initialState;
+ }
+
+ private getViewExitState(view: View): TransitionAnimation {
+ const popupIndex = this.getPopupIndex(view);
+ if (popupIndex === -1) {
+ return;
+ }
+ const exitAnimation = this.popupViews[popupIndex]?.options?.animation?.exitTo;
+ if (!exitAnimation) {
+ return;
+ }
+ return exitAnimation;
+ }
+
+ private applyInitialState(targetView: View, enterFrom: TransitionAnimation): void {
+ const animationOptions = {
+ ...defaultTransitionAnimation,
+ ...enterFrom,
+ };
+ targetView.translateX = animationOptions.translateX;
+ targetView.translateY = animationOptions.translateY;
+ targetView.scaleX = animationOptions.scaleX;
+ targetView.scaleY = animationOptions.scaleY;
+ targetView.rotate = animationOptions.rotate;
+ targetView.opacity = animationOptions.opacity;
+ }
+
+ private applyDefaultState(targetView: View): void {
+ targetView.translateX = 0;
+ targetView.translateY = 0;
+ targetView.scaleX = 1;
+ targetView.scaleY = 1;
+ targetView.rotate = 0;
+ targetView.opacity = 1;
+ }
+
+ private getEnterAnimation(targetView: View, enterFrom: TransitionAnimation): Animation {
+ const animationOptions = {
+ ...defaultTransitionAnimation,
+ ...enterFrom,
+ };
+ return new Animation([
+ {
+ target: targetView,
+ translate: { x: 0, y: 0 },
+ scale: { x: 1, y: 1 },
+ rotate: 0,
+ opacity: 1,
+ duration: animationOptions.duration,
+ curve: animationOptions.curve,
+ },
+ ]);
+ }
+
+ private getExitAnimation(targetView: View, exitTo: TransitionAnimation): Animation {
+ const animationOptions = {
+ ...defaultTransitionAnimation,
+ ...exitTo,
+ };
+ return new Animation([
+ {
+ target: targetView,
+ translate: { x: animationOptions.translateX, y: animationOptions.translateY },
+ scale: { x: animationOptions.scaleX, y: animationOptions.scaleY },
+ rotate: animationOptions.rotate,
+ opacity: animationOptions.opacity,
+ duration: animationOptions.duration,
+ curve: animationOptions.curve,
+ },
+ ]);
+ }
+
+ private createShadeCover(shadeOptions: ShadeCoverOptions): View {
+ const shadeCover = new GridLayout();
+ shadeCover.verticalAlignment = 'bottom';
+ shadeCover.on('loaded', () => {
+ this._initShadeCover(shadeCover, shadeOptions);
+ this.updateShadeCover(shadeCover, shadeOptions);
+ });
+ return shadeCover;
+ }
+
+ private updateShadeCover(shade: View, shadeOptions: ShadeCoverOptions): void {
+ if (shadeOptions.tapToClose !== undefined && shadeOptions.tapToClose !== null) {
+ shade.off('tap');
+ if (shadeOptions.tapToClose) {
+ shade.on('tap', () => {
+ this.closeAll();
+ });
+ }
+ }
+ this._updateShadeCover(shade, shadeOptions);
+ }
+
+ private hasChild(view: View): boolean {
+ return this.getChildIndex(view) >= 0;
+ }
+
+ private closeShadeCover(shadeCoverOptions?: ShadeCoverOptions): Promise {
+ 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(() => {
+ this.removeChild(this.shadeCover);
+ this.shadeCover.off('loaded');
+ this.shadeCover = null;
+ resolve();
+ });
+ }
+ });
+ }
+
+ protected _bringToFront(view: View) {}
+
+ protected _initShadeCover(view: View, shadeOption: ShadeCoverOptions): void {}
+
+ protected _updateShadeCover(view: View, shadeOption: ShadeCoverOptions): Promise {
+ return new Promise(() => {});
+ }
+
+ protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise {
+ return new Promise(() => {});
+ }
+}
+
+export function getRootLayout(): RootLayout {
+ return global.rootLayout;
+}
+
+export const defaultTransitionAnimation: TransitionAnimation = {
+ translateX: 0,
+ translateY: 0,
+ scaleX: 1,
+ scaleY: 1,
+ rotate: 0,
+ opacity: 1,
+ duration: 300,
+ curve: AnimationCurve.easeIn,
+};
+
+export const defaultShadeCoverTransitionAnimation: TransitionAnimation = {
+ ...defaultTransitionAnimation,
+ opacity: 0, // default to fade in/out
+};
+
+export const defaultShadeCoverOptions: ShadeCoverOptions = {
+ opacity: 0.5,
+ color: '#000000',
+ tapToClose: true,
+ animation: {
+ enterFrom: defaultShadeCoverTransitionAnimation,
+ exitTo: defaultShadeCoverTransitionAnimation,
+ },
+ ignoreShadeRestore: false,
+};
diff --git a/packages/webpack/templates/webpack.react.js b/packages/webpack/templates/webpack.react.js
index d9c3f90627..adb591fb64 100644
--- a/packages/webpack/templates/webpack.react.js
+++ b/packages/webpack/templates/webpack.react.js
@@ -85,4 +85,4 @@ module.exports = (env) => {
}
return baseConfig;
-};
\ No newline at end of file
+};
diff --git a/workspace.json b/workspace.json
index 7c23c1ef4a..903ff0b5a8 100644
--- a/workspace.json
+++ b/workspace.json
@@ -313,4 +313,4 @@
}
}
}
-}
+}
\ No newline at end of file