feat(core): RootLayout with api to fluidly handle dynamic layers (#8980)

This commit is contained in:
William Tjondrosuharto
2021-01-30 02:49:32 +07:00
committed by Nathan Walker
parent a6b1bde655
commit a90609a670
10 changed files with 821 additions and 41 deletions

View File

@ -3,49 +3,84 @@
<ActionBar title="Dev Toolbox" icon="" class="action-bar">
</ActionBar>
</Page.actionBar>
<ScrollView>
<StackLayout class="p-t-20 p-x-20">
<!-- <Button text="Button with html boxShadow" tap="{{ onTap }}" height="50" class="btn btn-primary btn-active" boxShadow="5 5 10 navy"/>
<Button text="Button with css boxShadow (rgba)" tap="{{ onTap }}" height="50" marginTop="50" class="btn btn-primary btn-active"/>
<Button text="Button with boxShadow 3 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 navy" class="btn btn-primary btn-active"/>
<Button text="Button with boxShadow 4 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 navy" class="btn btn-primary btn-active"/>
<Button text="Button with boxShadow 5 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 10 navy" class="btn btn-primary btn-active"/>
<StackLayout boxShadow="10 10 rgba(0,0,0,1)" marginTop="50">
<Button text="Button with css boxShadow" tap="{{ onTap }}" height="50" borderRadius="10"/>
</StackLayout> -->
<!-- TODO: if backgroundColor is not set, it won't call background.ios.ts hence not applying the boxShadow -->
<!-- <StackLayout boxShadow="5 5 10 10 red" height="100" backgroundColor="transparent" padding="10" margin="20">
<Label text="StackLayout with transparent background"></Label>
</StackLayout> -->
<GridLayout boxShadow="10 -10 10 10 rgba(0,0,0,0.5)" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="GridLayout"></Label>
<GridLayout rows="2*, *">
<!-- background color transparent here to hide children overflow -->
<AbsoluteLayout row="0" backgroundColor="transparent">
<!-- Root layout demo -->
<RootLayout height="100%" width="100%">
<GridLayout height="100%" backgroundColor="#232652">
<Label verticalAlignment="center" textAlignment="center" fontWeight="bold" color="#fff" text="ROOT LAYOUT CONTENT"></Label>
</GridLayout>
</RootLayout>
</AbsoluteLayout>
<StackLayout boxShadow="5 10 10 20 #000" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="StackLayout"></Label>
</StackLayout>
<!-- Root layout controls -->
<StackLayout row="1">
<ScrollView height="100%">
<StackLayout class="p-15">
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="ORANGE"></Label>
<FlexboxLayout flexDirection="row" justifyContent="space-between">
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="0" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="0" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="0" class="btn btn-primary btn-active"/>
</FlexboxLayout>
<AbsoluteLayout boxShadow="5 15 10 20 green" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="AbsoluteLayout"></Label>
</AbsoluteLayout>
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="NAVY"></Label>
<FlexboxLayout flexDirection="row">
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="1" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="1" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="1" class="btn btn-primary btn-active"/>
</FlexboxLayout>
<!-- note: the 3rd number in box shadow is currently being ignored, only the 1st, 2nd, and 4th, and color are being used-->
<FlexboxLayout boxShadow="0 0 10 25 red" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="FlexboxLayout"></Label>
</FlexboxLayout>
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="GRAY"></Label>
<FlexboxLayout flexDirection="row">
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="2" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="2" class="btn btn-primary btn-active"/>
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="2" class="btn btn-primary btn-active"/>
</FlexboxLayout>
<FlexboxLayout boxShadow="15 10 10 20 #000" height="100" backgroundColor="transparent" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="FlexboxLayout (transparent background)"></Label>
</FlexboxLayout>
<!-- <Button text="Button with html boxShadow" tap="{{ onTap }}" height="50" class="btn btn-primary btn-active" boxShadow="5 5 10 navy"/>
<Button marginTop="30" boxShadow="0 0 10 8 #000" backgroundColor="transparent" text="button" padding="20"></Button>
<Button text="Button with css boxShadow (rgba)" tap="{{ onTap }}" height="50" marginTop="50" class="btn btn-primary btn-active"/>
</StackLayout>
</ScrollView>
<Button text="Button with boxShadow 3 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 navy" class="btn btn-primary btn-active"/>
<Button text="Button with boxShadow 4 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 navy" class="btn btn-primary btn-active"/>
<Button text="Button with boxShadow 5 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 10 navy" class="btn btn-primary btn-active"/>
<StackLayout boxShadow="10 10 rgba(0,0,0,1)" marginTop="50">
<Button text="Button with css boxShadow" tap="{{ onTap }}" height="50" borderRadius="10"/>
</StackLayout> -->
<!-- TODO: if backgroundColor is not set, it won't call background.ios.ts hence not applying the boxShadow -->
<!-- <StackLayout boxShadow="5 5 10 10 red" height="100" backgroundColor="transparent" padding="10" margin="20">
<Label text="StackLayout with transparent background"></Label>
</StackLayout> -->
<GridLayout boxShadow="10 -10 10 10 rgba(0,0,0,0.5)" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="GridLayout"></Label>
</GridLayout>
<StackLayout boxShadow="5 10 10 20 #000" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="StackLayout"></Label>
</StackLayout>
<AbsoluteLayout boxShadow="5 15 10 20 green" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="AbsoluteLayout"></Label>
</AbsoluteLayout>
<!-- note: the 3rd number in box shadow is currently being ignored, only the 1st, 2nd, and 4th, and color are being used-->
<FlexboxLayout boxShadow="0 0 10 25 red" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="FlexboxLayout"></Label>
</FlexboxLayout>
<FlexboxLayout boxShadow="15 10 10 20 #000" height="100" backgroundColor="transparent" padding="10" margin="20" tap="{{ toggleAnimation }}">
<Label text="FlexboxLayout (transparent background)"></Label>
</FlexboxLayout>
<Button marginTop="30" boxShadow="0 0 10 8 #000" backgroundColor="transparent" text="button" padding="20"></Button>
</StackLayout>
</ScrollView>
</StackLayout>
</GridLayout>
</Page>

View File

@ -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[(<any>args.object).popupIndex].view, this.popupViews[(<any>args.object).popupIndex].options)
.then(() => console.log('opened'))
.catch((ex) => console.error(ex));
}
bringToFront(args: EventData): void {
getRootLayout()
.bringToFront(this.popupViews[(<any>args.object).popupIndex].view, true)
.then(() => console.log('brought to front'))
.catch((ex) => console.error(ex));
}
close(args: EventData): void {
if (this.popupViews[(<any>args.object).popupIndex]?.extra?.customExitAnimation) {
getRootLayout()
.close(this.popupViews[(<any>args.object).popupIndex].view, this.popupViews[(<any>args.object).popupIndex].extra.customExitAnimation)
.then(() => console.log('closed with custom exit animation'))
.catch((ex) => console.error(ex));
} else {
getRootLayout()
.close(this.popupViews[(<any>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',

View File

@ -123,6 +123,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;
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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) {
(<android.view.View>view.nativeViewProtected).bringToFront();
}
protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
const initialState = <TransitionAnimation>{
...defaultShadeCoverOptions.animation.enterFrom,
...shadeOptions?.animation?.enterFrom,
};
this._playAnimation(this._getAnimationSet(view, initialState));
}
protected _updateShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
const options = <ShadeCoverOptions>{
...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<void> {
const exitState = <TransitionAnimation>{
...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<android.animation.Animator> {
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((<Color>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 = (<java.lang.Integer>animator.getAnimatedValue()).intValue();
view.backgroundColor = new Color(argb);
},
})
);
return backgroundColorAnimator;
}
private _playAnimation(animationSet: Array<android.animation.Animator>, duration: number = 0): Promise<void> {
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();
});
}
}

View File

@ -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<void>;
close(view: View, exitTo?: TransitionAnimation): Promise<void>;
bringToFront(view: View, animated?: boolean): Promise<void>;
closeAll(): Promise<void>;
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;
}

View File

@ -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) {
(<UIView>this.nativeViewProtected).bringSubviewToFront(view.nativeViewProtected);
}
protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
const initialState = <TransitionAnimation>{
...defaultShadeCoverOptions.animation.enterFrom,
...shadeOptions?.animation?.enterFrom,
};
this._applyAnimationProperties(view, initialState);
}
protected _updateShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
return new Promise((resolve) => {
const options = <ShadeCoverOptions>{
...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<void> {
return new Promise((resolve) => {
const exitState = <TransitionAnimation>{
...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;
}
}

View File

@ -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 <RootLayout> 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<void> {
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<void> {
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<any>[] = [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<void> {
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<void> {
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<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(() => {
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<void> {
return new Promise(() => {});
}
protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
return new Promise(() => {});
}
}
export function getRootLayout(): RootLayout {
return <RootLayout>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,
};