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