Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/toolbox/src/pages/root-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class RootLayoutModel extends Observable {
view: this.getPopup('#EA5936', 110, -30),
options: {
shadeCover: {
color: '#FFF',
color: 'linear-gradient(to bottom, red, blue)',
opacity: 0.7,
tapToClose: true,
},
Expand Down
2 changes: 1 addition & 1 deletion packages/core/ui/core/view/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EventData } from '../../../data/observable';
import { Color } from '../../../color';
import { Animation, AnimationDefinition, AnimationPromise } from '../../animation';
import { GestureTypes, GesturesObserver } from '../../gestures';
import { LinearGradient } from '../../styling/gradient';
import { LinearGradient } from '../../styling/linear-gradient';
import { AccessibilityLiveRegion, AccessibilityRole, AccessibilityState, AccessibilityTrait, AccessibilityEventOptions } from '../../../accessibility/accessibility-types';
import { CoreTypes } from '../../../core-types';
import { CSSShadow } from '../../styling/css-shadow';
Expand Down
20 changes: 18 additions & 2 deletions packages/core/ui/layouts/root-layout/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Color } from '../../../color';
import { View } from '../../core/view';
import { RootLayoutBase, defaultShadeCoverOptions } from './root-layout-common';
import { TransitionAnimation, ShadeCoverOptions } from '.';
import { parseLinearGradient } from '../../../css/parser';
import { LinearGradient } from '../../styling/linear-gradient';

export * from './root-layout-common';

Expand Down Expand Up @@ -74,14 +76,28 @@ export class RootLayout extends RootLayoutBase {
}

private _getAnimationSet(view: View, shadeCoverAnimation: TransitionAnimation, backgroundColor: string = defaultShadeCoverOptions.color): Array<android.animation.Animator> {
const animationSet = Array.create(android.animation.Animator, 7);
const backgroundIsGradient = backgroundColor.startsWith('linear-gradient');

const animationSet = Array.create(android.animation.Animator, backgroundIsGradient ? 6 : 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);

if (backgroundIsGradient) {
if (view.backgroundColor) {
view.backgroundColor = undefined;
}
const parsedGradient = parseLinearGradient(backgroundColor);
view.backgroundImage = LinearGradient.parse(parsedGradient.value);
} else {
if (view.backgroundImage) {
view.backgroundImage = undefined;
}
animationSet[6] = this._getBackgroundColorAnimator(view, backgroundColor);
}
return animationSet;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/ui/layouts/root-layout/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export class RootLayout extends GridLayout {
bringToFront(view: View, animated?: boolean): Promise<void>;
closeAll(): Promise<void>;
getShadeCover(): View;
openShadeCover(options: ShadeCoverOptions): void;
closeShadeCover(shadeCoverOptions?: ShadeCoverOptions): Promise<void>;
}

export function getRootLayout(): RootLayout;
Expand Down
44 changes: 35 additions & 9 deletions packages/core/ui/layouts/root-layout/index.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import { Color } from '../../../color';
import { View } from '../../core/view';
import { RootLayoutBase, defaultShadeCoverOptions } from './root-layout-common';
import { TransitionAnimation, ShadeCoverOptions } from '.';
import { LinearGradient } from '../../styling/linear-gradient';
import { ios as iosViewUtils } from '../../utils';
import { parseLinearGradient } from '../../../css/parser';
export * from './root-layout-common';

export class RootLayout extends RootLayoutBase {
// perf optimization: only create and insert gradients if settings change
private _currentGradient: string;
private _gradientLayer: CAGradientLayer;

constructor() {
super();
}
Expand All @@ -27,12 +34,24 @@ export class RootLayout extends RootLayoutBase {
...defaultShadeCoverOptions,
...shadeOptions,
};
if (view && view.nativeViewProtected) {
if (view?.nativeViewProtected) {
const duration = this._convertDurationToSeconds(options.animation?.enterFrom?.duration || defaultShadeCoverOptions.animation.enterFrom.duration);

if (options.color && options.color.startsWith('linear-gradient')) {
if (options.color !== this._currentGradient) {
this._currentGradient = options.color;
const parsedGradient = parseLinearGradient(options.color);
this._gradientLayer = iosViewUtils.drawGradient(view.nativeViewProtected, LinearGradient.parse(parsedGradient.value), 0);
}
}
UIView.animateWithDurationAnimationsCompletion(
duration,
() => {
view.nativeViewProtected.backgroundColor = new Color(options.color).ios;
if (this._gradientLayer) {
this._gradientLayer.opacity = 1;
} else if (options.color && view?.nativeViewProtected) {
view.nativeViewProtected.backgroundColor = new Color(options.color).ios;
}
this._applyAnimationProperties(view, {
translateX: 0,
translateY: 0,
Expand Down Expand Up @@ -71,14 +90,21 @@ export class RootLayout extends RootLayoutBase {
});
}

protected _cleanupPlatformShadeCover(): void {
this._currentGradient = null;
this._gradientLayer = null;
}

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;
if (view?.nativeViewProtected) {
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 {
Expand Down
152 changes: 84 additions & 68 deletions packages/core/ui/layouts/root-layout/root-layout-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CSSType, View } from '../../core/view';
import { GridLayout } from '../grid-layout';
import { RootLayout, RootLayoutOptions, ShadeCoverOptions, TransitionAnimation } from '.';
import { Animation } from '../../animation';
import { AnimationDefinition } from '../../animation';

@CSSType('RootLayout')
export class RootLayoutBase extends GridLayout {
Expand Down Expand Up @@ -33,16 +34,15 @@ export class RootLayoutBase extends GridLayout {
// 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);
if (options?.shadeCover) {
// perf optimization note: we only need 1 layer of shade cover
// we just update properties if needed by additional overlaid views
if (this.shadeCover) {
// overwrite current shadeCover options if topmost popupview has additional shadeCover configurations
this.updateShadeCover(this.shadeCover, options.shadeCover);
} else {
this.openShadeCover(options.shadeCover);
}
}

view.opacity = 0; // always begin with view invisible when adding dynamically
Expand Down Expand Up @@ -77,47 +77,46 @@ export class RootLayoutBase extends GridLayout {
close(view: View, exitTo?: TransitionAnimation): Promise<void> {
return new Promise((resolve, reject) => {
if (this.hasChild(view)) {
const cleanupAndFinish = () => {
this.removeChild(view);
resolve();
};

try {
const popupIndex = this.getPopupIndex(view);
const poppedView = this.popupViews[popupIndex];
// use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening
const exitAnimationDefinition = exitTo || this.popupViews[popupIndex]?.options?.animation?.exitTo;
const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo;

// Remove view from local array
const poppedView = this.popupViews[popupIndex];
// Remove view from tracked popupviews
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 (this.shadeCover && shadeCoverOptions && !poppedView?.options?.shadeCover.ignoreShadeRestore) {
this.updateShadeCover(this.shadeCover, shadeCoverOptions);
if (this.shadeCover) {
// update shade cover with the topmost popupView options (if not specifically told to ignore)
if (!poppedView?.options?.shadeCover.ignoreShadeRestore) {
const shadeCoverOptions = this.popupViews[this.popupViews.length - 1]?.options?.shadeCover;
if (shadeCoverOptions) {
this.updateShadeCover(this.shadeCover, shadeCoverOptions);
}
}
// remove shade cover animation if this is the last opened popup view
if (this.popupViews.length === 0) {
this.closeShadeCover(poppedView.options.shadeCover);
}
}

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 && this.shadeCover) {
exitAnimations.push(this.closeShadeCover(poppedView.options.shadeCover));
}
return Promise.all(exitAnimations)
.then(() => {
this.removeChild(view);
resolve();
})
this.getExitAnimation(view, exitAnimationDefinition)
.play()
.then(cleanupAndFinish.bind(this))
.catch((ex) => {
if (Trace.isEnabled()) {
Trace.write(`Error playing exit animation: ${ex}`, Trace.categories.Layout, Trace.messageType.error);
}
});
} else {
cleanupAndFinish();
}
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);
Expand Down Expand Up @@ -147,6 +146,44 @@ export class RootLayoutBase extends GridLayout {
});
}

getShadeCover(): View {
return this.shadeCover;
}

openShadeCover(options: ShadeCoverOptions) {
if (this.shadeCover) {
if (Trace.isEnabled()) {
Trace.write(`RootLayout shadeCover already open.`, Trace.categories.Layout, Trace.messageType.warn);
}
} else {
// create the one and only shade cover
this.shadeCover = this.createShadeCover(options);
// insert shade cover at index right above the first layout
this.insertChild(this.shadeCover, this.staticChildCount + 1);
}
}

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(() => {
if (this.shadeCover) {
this.shadeCover.off('loaded');
if (this.shadeCover.parent) {
this.removeChild(this.shadeCover);
}
}
this.shadeCover = null;
// cleanup any platform specific details related to shade cover
this._cleanupPlatformShadeCover();
resolve();
});
}
resolve();
});
}

// 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) => {
Expand Down Expand Up @@ -214,10 +251,6 @@ export class RootLayoutBase extends GridLayout {
});
}

getShadeCover(): View {
return this.shadeCover;
}

private getPopupIndex(view: View): number {
return this.popupViews.findIndex((popupView) => popupView.view === view);
}
Expand Down Expand Up @@ -287,21 +320,17 @@ export class RootLayoutBase extends GridLayout {
}

private getExitAnimation(targetView: View, exitTo: TransitionAnimation): Animation {
const animationOptions = {
return new Animation([this.getExitAnimationDefinition(targetView, exitTo)]);
}

private getExitAnimationDefinition(targetView: View, exitTo: TransitionAnimation): AnimationDefinition {
return {
target: targetView,
...defaultTransitionAnimation,
...exitTo,
...(exitTo || {}),
translate: { x: exitTo.translateX || defaultTransitionAnimation.translateX, y: exitTo.translateY || defaultTransitionAnimation.translateY },
scale: { x: exitTo.scaleX || defaultTransitionAnimation.scaleX, y: exitTo.scaleY || defaultTransitionAnimation.scaleY },
};
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 {
Expand Down Expand Up @@ -330,21 +359,6 @@ export class RootLayoutBase extends GridLayout {
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();
});
}
resolve();
});
}

protected _bringToFront(view: View) {}

protected _initShadeCover(view: View, shadeOption: ShadeCoverOptions): void {}
Expand All @@ -356,6 +370,8 @@ export class RootLayoutBase extends GridLayout {
protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise<void> {
return new Promise(() => {});
}

protected _cleanupPlatformShadeCover(): void {}
}

export function getRootLayout(): RootLayout {
Expand Down
Loading