mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-26 11:17:04 +08:00
feat(core): RootLayout with api to fluidly handle dynamic layers (#8980)
This commit is contained in:

committed by
Nathan Walker

parent
a6b1bde655
commit
a90609a670
@ -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>
|
||||
|
@ -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',
|
||||
|
3
packages/core/global-types.d.ts
vendored
3
packages/core/global-types.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
1
packages/core/ui/layouts/index.d.ts
vendored
1
packages/core/ui/layouts/index.d.ts
vendored
@ -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';
|
||||
|
@ -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';
|
||||
|
102
packages/core/ui/layouts/root-layout/index.android.ts
Normal file
102
packages/core/ui/layouts/root-layout/index.android.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
43
packages/core/ui/layouts/root-layout/index.d.ts
vendored
Normal file
43
packages/core/ui/layouts/root-layout/index.d.ts
vendored
Normal 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;
|
||||
}
|
87
packages/core/ui/layouts/root-layout/index.ios.ts
Normal file
87
packages/core/ui/layouts/root-layout/index.ios.ts
Normal 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;
|
||||
}
|
||||
}
|
389
packages/core/ui/layouts/root-layout/root-layout-common.ts
Normal file
389
packages/core/ui/layouts/root-layout/root-layout-common.ts
Normal 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,
|
||||
};
|
Reference in New Issue
Block a user