Skip to content

Commit 577b1e9

Browse files
williamjuan027NathanWalker
authored andcommitted
feat(core): RootLayout with api to fluidly handle dynamic layers (#8980)
1 parent cbba5ed commit 577b1e9

File tree

10 files changed

+817
-37
lines changed

10 files changed

+817
-37
lines changed

apps/toolbox/src/main-page.xml

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,84 @@
33
<ActionBar title="Dev Toolbox" icon="" class="action-bar">
44
</ActionBar>
55
</Page.actionBar>
6-
<ScrollView>
7-
<StackLayout class="p-t-20 p-x-20">
6+
7+
<GridLayout rows="2*, *">
8+
<!-- background color transparent here to hide children overflow -->
9+
<AbsoluteLayout row="0" backgroundColor="transparent">
10+
<!-- Root layout demo -->
11+
<RootLayout height="100%" width="100%">
12+
<GridLayout height="100%" backgroundColor="#232652">
13+
<Label verticalAlignment="center" textAlignment="center" fontWeight="bold" color="#fff" text="ROOT LAYOUT CONTENT"></Label>
14+
</GridLayout>
15+
</RootLayout>
16+
</AbsoluteLayout>
817

9-
<!-- <Button text="Button with html boxShadow" tap="{{ onTap }}" height="50" class="btn btn-primary btn-active" boxShadow="5 5 10 navy"/>
18+
<!-- Root layout controls -->
19+
<StackLayout row="1">
20+
<ScrollView height="100%">
21+
<StackLayout class="p-15">
22+
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="ORANGE"></Label>
23+
<FlexboxLayout flexDirection="row" justifyContent="space-between">
24+
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="0" class="btn btn-primary btn-active"/>
25+
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="0" class="btn btn-primary btn-active"/>
26+
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="0" class="btn btn-primary btn-active"/>
27+
</FlexboxLayout>
1028

11-
<Button text="Button with css boxShadow (rgba)" tap="{{ onTap }}" height="50" marginTop="50" class="btn btn-primary btn-active"/>
12-
13-
<Button text="Button with boxShadow 3 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 navy" class="btn btn-primary btn-active"/>
14-
<Button text="Button with boxShadow 4 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 navy" class="btn btn-primary btn-active"/>
15-
<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"/>
29+
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="NAVY"></Label>
30+
<FlexboxLayout flexDirection="row">
31+
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="1" class="btn btn-primary btn-active"/>
32+
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="1" class="btn btn-primary btn-active"/>
33+
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="1" class="btn btn-primary btn-active"/>
34+
</FlexboxLayout>
1635

17-
<StackLayout boxShadow="10 10 rgba(0,0,0,1)" marginTop="50">
18-
<Button text="Button with css boxShadow" tap="{{ onTap }}" height="50" borderRadius="10"/>
19-
</StackLayout> -->
36+
<Label color="#b4b6b9" fontSize="25" fontWeight="bold" text="GRAY"></Label>
37+
<FlexboxLayout flexDirection="row">
38+
<Button flexGrow="1" text="open" tap="{{ open }}" popupIndex="2" class="btn btn-primary btn-active"/>
39+
<Button flexGrow="1" text="front" tap="{{ bringToFront }}" popupIndex="2" class="btn btn-primary btn-active"/>
40+
<Button flexGrow="1" text="close" tap="{{ close }}" popupIndex="2" class="btn btn-primary btn-active"/>
41+
</FlexboxLayout>
42+
43+
<!-- <Button text="Button with html boxShadow" tap="{{ onTap }}" height="50" class="btn btn-primary btn-active" boxShadow="5 5 10 navy"/>
2044
21-
<!-- TODO: if backgroundColor is not set, it won't call background.ios.ts hence not applying the boxShadow -->
22-
<!-- <StackLayout boxShadow="5 5 10 10 red" height="100" backgroundColor="transparent" padding="10" margin="20">
23-
<Label text="StackLayout with transparent background"></Label>
24-
</StackLayout> -->
45+
<Button text="Button with css boxShadow (rgba)" tap="{{ onTap }}" height="50" marginTop="50" class="btn btn-primary btn-active"/>
2546
26-
<GridLayout boxShadow="10 -10 10 10 rgba(0,0,0,0.5)" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
27-
<Label text="GridLayout"></Label>
28-
</GridLayout>
47+
<Button text="Button with boxShadow 3 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 navy" class="btn btn-primary btn-active"/>
48+
<Button text="Button with boxShadow 4 props" tap="{{ onTap }}" height="50" marginTop="50" boxShadow="5 5 10 navy" class="btn btn-primary btn-active"/>
49+
<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"/>
50+
51+
<StackLayout boxShadow="10 10 rgba(0,0,0,1)" marginTop="50">
52+
<Button text="Button with css boxShadow" tap="{{ onTap }}" height="50" borderRadius="10"/>
53+
</StackLayout> -->
54+
55+
<!-- TODO: if backgroundColor is not set, it won't call background.ios.ts hence not applying the boxShadow -->
56+
<!-- <StackLayout boxShadow="5 5 10 10 red" height="100" backgroundColor="transparent" padding="10" margin="20">
57+
<Label text="StackLayout with transparent background"></Label>
58+
</StackLayout> -->
59+
60+
<GridLayout boxShadow="10 -10 10 10 rgba(0,0,0,0.5)" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
61+
<Label text="GridLayout"></Label>
62+
</GridLayout>
2963

30-
<StackLayout boxShadow="5 10 10 20 #000" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
31-
<Label text="StackLayout"></Label>
32-
</StackLayout>
64+
<StackLayout boxShadow="5 10 10 20 #000" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
65+
<Label text="StackLayout"></Label>
66+
</StackLayout>
3367

34-
<AbsoluteLayout boxShadow="5 15 10 20 green" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
35-
<Label text="AbsoluteLayout"></Label>
36-
</AbsoluteLayout>
68+
<AbsoluteLayout boxShadow="5 15 10 20 green" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
69+
<Label text="AbsoluteLayout"></Label>
70+
</AbsoluteLayout>
3771

38-
<!-- note: the 3rd number in box shadow is currently being ignored, only the 1st, 2nd, and 4th, and color are being used-->
39-
<FlexboxLayout boxShadow="0 0 10 25 red" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
40-
<Label text="FlexboxLayout"></Label>
41-
</FlexboxLayout>
72+
<!-- note: the 3rd number in box shadow is currently being ignored, only the 1st, 2nd, and 4th, and color are being used-->
73+
<FlexboxLayout boxShadow="0 0 10 25 red" height="100" backgroundColor="lightblue" padding="10" margin="20" tap="{{ toggleAnimation }}">
74+
<Label text="FlexboxLayout"></Label>
75+
</FlexboxLayout>
4276

43-
<FlexboxLayout boxShadow="15 10 10 20 #000" height="100" backgroundColor="transparent" padding="10" margin="20" tap="{{ toggleAnimation }}">
44-
<Label text="FlexboxLayout (transparent background)"></Label>
45-
</FlexboxLayout>
77+
<FlexboxLayout boxShadow="15 10 10 20 #000" height="100" backgroundColor="transparent" padding="10" margin="20" tap="{{ toggleAnimation }}">
78+
<Label text="FlexboxLayout (transparent background)"></Label>
79+
</FlexboxLayout>
4680

47-
<Button marginTop="30" boxShadow="0 0 10 8 #000" backgroundColor="transparent" text="button" padding="20"></Button>
48-
49-
</StackLayout>
50-
</ScrollView>
81+
<Button marginTop="30" boxShadow="0 0 10 8 #000" backgroundColor="transparent" text="button" padding="20"></Button>
82+
</StackLayout>
83+
</ScrollView>
84+
</StackLayout>
85+
</GridLayout>
5186
</Page>

apps/toolbox/src/main-view-model.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Observable, Frame, StackLayout } from '@nativescript/core';
1+
import { Observable, Frame, View, StackLayout, getRootLayout, EventData, RootLayout, RootLayoutOptions } from '@nativescript/core';
2+
import { AnimationCurve } from '@nativescript/core/ui/enums';
23

34
export class HelloWorldModel extends Observable {
45
private _counter: number;
@@ -37,6 +38,123 @@ export class HelloWorldModel extends Observable {
3738
this.updateMessage();
3839
}
3940

41+
popupViews: { view: View; options: RootLayoutOptions; extra?: any }[] = [
42+
{
43+
view: this.getPopup('#EA5936', 110, -30),
44+
options: {
45+
shadeCover: {
46+
color: '#FFF',
47+
opacity: 0.7,
48+
tapToClose: true,
49+
},
50+
animation: {
51+
enterFrom: {
52+
opacity: 0,
53+
translateY: 500,
54+
duration: 500,
55+
},
56+
exitTo: {
57+
opacity: 0,
58+
duration: 300,
59+
},
60+
},
61+
},
62+
extra: {
63+
customExitAnimation: {
64+
opacity: 0,
65+
translate: { x: 0, y: -500 },
66+
},
67+
},
68+
},
69+
{
70+
view: this.getPopup('#232652', 110, 0),
71+
options: {
72+
shadeCover: {
73+
color: 'pink',
74+
opacity: 0.7,
75+
tapToClose: false,
76+
animation: {
77+
exitTo: {
78+
scaleX: 0,
79+
},
80+
},
81+
},
82+
},
83+
},
84+
{
85+
view: this.getPopup('#E1E4E8', 110, 30),
86+
options: {
87+
shadeCover: {
88+
color: '#ffffdd',
89+
opacity: 0.5,
90+
tapToClose: true,
91+
ignoreShadeRestore: true,
92+
animation: {
93+
enterFrom: {
94+
translateX: -1000,
95+
duration: 500,
96+
},
97+
exitTo: {
98+
rotate: -180,
99+
duration: 500,
100+
},
101+
},
102+
},
103+
animation: {
104+
enterFrom: {
105+
rotate: 180,
106+
duration: 300,
107+
},
108+
exitTo: {
109+
rotate: 180,
110+
opacity: 0,
111+
duration: 300,
112+
curve: AnimationCurve.spring,
113+
},
114+
},
115+
},
116+
},
117+
];
118+
119+
open(args: EventData): void {
120+
getRootLayout()
121+
.open(this.popupViews[(<any>args.object).popupIndex].view, this.popupViews[(<any>args.object).popupIndex].options)
122+
.then(() => console.log('opened'))
123+
.catch((ex) => console.error(ex));
124+
}
125+
126+
bringToFront(args: EventData): void {
127+
getRootLayout()
128+
.bringToFront(this.popupViews[(<any>args.object).popupIndex].view, true)
129+
.then(() => console.log('brought to front'))
130+
.catch((ex) => console.error(ex));
131+
}
132+
133+
close(args: EventData): void {
134+
if (this.popupViews[(<any>args.object).popupIndex]?.extra?.customExitAnimation) {
135+
getRootLayout()
136+
.close(this.popupViews[(<any>args.object).popupIndex].view, this.popupViews[(<any>args.object).popupIndex].extra.customExitAnimation)
137+
.then(() => console.log('closed with custom exit animation'))
138+
.catch((ex) => console.error(ex));
139+
} else {
140+
getRootLayout()
141+
.close(this.popupViews[(<any>args.object).popupIndex].view)
142+
.then(() => console.log('closed'))
143+
.catch((ex) => console.error(ex));
144+
}
145+
}
146+
147+
getPopup(color: string, size: number, offset: number): View {
148+
const layout = new StackLayout();
149+
layout.height = size;
150+
layout.width = size;
151+
layout.marginTop = offset;
152+
layout.marginLeft = offset;
153+
layout.backgroundColor = color;
154+
layout.borderRadius = 10;
155+
return layout;
156+
}
157+
40158
viewList() {
41159
Frame.topmost().navigate({
42160
moduleName: 'list-page',

packages/core/global-types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ declare namespace NodeJS {
126126
isIOS?: boolean;
127127
isAndroid?: boolean;
128128
__requireOverride?: (name: string, dir: string) => any;
129+
130+
// used to get the rootlayout instance to add/remove childviews
131+
rootLayout: any;
129132
}
130133
}
131134

packages/core/ui/layouts/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { AbsoluteLayout } from './absolute-layout';
22
export { DockLayout } from './dock-layout';
33
export { FlexboxLayout } from './flexbox-layout';
44
export { GridLayout, GridUnitType, ItemSpec } from './grid-layout';
5+
export { RootLayout, getRootLayout, RootLayoutOptions, ShadeCoverOptions } from './root-layout';
56
export { StackLayout } from './stack-layout';
67
export { WrapLayout } from './wrap-layout';
78
export { LayoutBase } from './layout-base';

packages/core/ui/layouts/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export { AbsoluteLayout } from './absolute-layout';
22
export { DockLayout } from './dock-layout';
33
export { FlexboxLayout } from './flexbox-layout';
44
export { GridLayout, GridUnitType, ItemSpec } from './grid-layout';
5+
export { RootLayout, getRootLayout } from './root-layout';
6+
export type { RootLayoutOptions, ShadeCoverOptions } from './root-layout';
57
export { StackLayout } from './stack-layout';
68
export { WrapLayout } from './wrap-layout';
79
export { LayoutBase } from './layout-base';
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Color } from '../../../color';
2+
import { View } from '../../core/view';
3+
import { RootLayoutBase, defaultShadeCoverOptions } from './root-layout-common';
4+
import { TransitionAnimation, ShadeCoverOptions } from '.';
5+
6+
export * from './root-layout-common';
7+
8+
export class RootLayout extends RootLayoutBase {
9+
constructor() {
10+
super();
11+
}
12+
13+
protected _bringToFront(view: View) {
14+
(<android.view.View>view.nativeViewProtected).bringToFront();
15+
}
16+
17+
protected _initShadeCover(view: View, shadeOptions: ShadeCoverOptions): void {
18+
const initialState = <TransitionAnimation>{
19+
...defaultShadeCoverOptions.animation.enterFrom,
20+
...shadeOptions?.animation?.enterFrom,
21+
};
22+
this._playAnimation(this._getAnimationSet(view, initialState));
23+
}
24+
25+
protected _updateShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
26+
const options = <ShadeCoverOptions>{
27+
...defaultShadeCoverOptions,
28+
...shadeOptions,
29+
};
30+
const duration = options.animation?.enterFrom?.duration || defaultShadeCoverOptions.animation.enterFrom.duration;
31+
return this._playAnimation(
32+
this._getAnimationSet(
33+
view,
34+
{
35+
translateX: 0,
36+
translateY: 0,
37+
scaleX: 1,
38+
scaleY: 1,
39+
rotate: 0,
40+
opacity: options.opacity,
41+
},
42+
options.color
43+
),
44+
duration
45+
);
46+
}
47+
48+
protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
49+
const exitState = <TransitionAnimation>{
50+
...defaultShadeCoverOptions.animation.exitTo,
51+
...shadeOptions?.animation?.exitTo,
52+
};
53+
return this._playAnimation(this._getAnimationSet(view, exitState), exitState?.duration);
54+
}
55+
56+
private _getAnimationSet(view: View, shadeCoverAnimation: TransitionAnimation, backgroundColor: string = defaultShadeCoverOptions.color): Array<android.animation.Animator> {
57+
const animationSet = Array.create(android.animation.Animator, 7);
58+
animationSet[0] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'translationX', [shadeCoverAnimation.translateX]);
59+
animationSet[1] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'translationY', [shadeCoverAnimation.translateY]);
60+
animationSet[2] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'scaleX', [shadeCoverAnimation.scaleX]);
61+
animationSet[3] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'scaleY', [shadeCoverAnimation.scaleY]);
62+
animationSet[4] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'rotation', [shadeCoverAnimation.rotate]);
63+
animationSet[5] = android.animation.ObjectAnimator.ofFloat(view.nativeViewProtected, 'alpha', [shadeCoverAnimation.opacity]);
64+
animationSet[6] = this._getBackgroundColorAnimator(view, backgroundColor);
65+
return animationSet;
66+
}
67+
68+
private _getBackgroundColorAnimator(view: View, backgroundColor: string): android.animation.ValueAnimator {
69+
const nativeArray = Array.create(java.lang.Object, 2);
70+
nativeArray[0] = view.backgroundColor ? java.lang.Integer.valueOf((<Color>view.backgroundColor).argb) : java.lang.Integer.valueOf(-1);
71+
nativeArray[1] = java.lang.Integer.valueOf(new Color(backgroundColor).argb);
72+
const backgroundColorAnimator = android.animation.ValueAnimator.ofObject(new android.animation.ArgbEvaluator(), nativeArray);
73+
backgroundColorAnimator.addUpdateListener(
74+
new android.animation.ValueAnimator.AnimatorUpdateListener({
75+
onAnimationUpdate(animator: android.animation.ValueAnimator) {
76+
let argb = (<java.lang.Integer>animator.getAnimatedValue()).intValue();
77+
view.backgroundColor = new Color(argb);
78+
},
79+
})
80+
);
81+
return backgroundColorAnimator;
82+
}
83+
84+
private _playAnimation(animationSet: Array<android.animation.Animator>, duration: number = 0): Promise<void> {
85+
return new Promise((resolve) => {
86+
const animatorSet = new android.animation.AnimatorSet();
87+
animatorSet.playTogether(animationSet);
88+
animatorSet.setDuration(duration);
89+
animatorSet.addListener(
90+
new android.animation.Animator.AnimatorListener({
91+
onAnimationStart: function (animator: android.animation.Animator): void {},
92+
onAnimationEnd: function (animator: android.animation.Animator): void {
93+
resolve();
94+
},
95+
onAnimationRepeat: function (animator: android.animation.Animator): void {},
96+
onAnimationCancel: function (animator: android.animation.Animator): void {},
97+
})
98+
);
99+
animatorSet.start();
100+
});
101+
}
102+
}

0 commit comments

Comments
 (0)