diff --git a/apps/toolbox/src/pages/root-layout.ts b/apps/toolbox/src/pages/root-layout.ts index 99a31db84..13531edd1 100644 --- a/apps/toolbox/src/pages/root-layout.ts +++ b/apps/toolbox/src/pages/root-layout.ts @@ -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, }, diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index f4d6a2445..80c548594 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -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'; diff --git a/packages/core/ui/layouts/root-layout/index.android.ts b/packages/core/ui/layouts/root-layout/index.android.ts index eb974a606..1d5748d88 100644 --- a/packages/core/ui/layouts/root-layout/index.android.ts +++ b/packages/core/ui/layouts/root-layout/index.android.ts @@ -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'; @@ -74,14 +76,28 @@ export class RootLayout extends RootLayoutBase { } private _getAnimationSet(view: View, shadeCoverAnimation: TransitionAnimation, backgroundColor: string = defaultShadeCoverOptions.color): Array { - 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; } diff --git a/packages/core/ui/layouts/root-layout/index.d.ts b/packages/core/ui/layouts/root-layout/index.d.ts index 86b9d1e37..34c3ae5fb 100644 --- a/packages/core/ui/layouts/root-layout/index.d.ts +++ b/packages/core/ui/layouts/root-layout/index.d.ts @@ -7,6 +7,8 @@ export class RootLayout extends GridLayout { bringToFront(view: View, animated?: boolean): Promise; closeAll(): Promise; getShadeCover(): View; + openShadeCover(options: ShadeCoverOptions): void; + closeShadeCover(shadeCoverOptions?: ShadeCoverOptions): Promise; } export function getRootLayout(): RootLayout; diff --git a/packages/core/ui/layouts/root-layout/index.ios.ts b/packages/core/ui/layouts/root-layout/index.ios.ts index bce344bd8..461963bbb 100644 --- a/packages/core/ui/layouts/root-layout/index.ios.ts +++ b/packages/core/ui/layouts/root-layout/index.ios.ts @@ -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(); } @@ -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, @@ -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 { diff --git a/packages/core/ui/layouts/root-layout/root-layout-common.ts b/packages/core/ui/layouts/root-layout/root-layout-common.ts index 86f0d3ee8..5049b7e4f 100644 --- a/packages/core/ui/layouts/root-layout/root-layout-common.ts +++ b/packages/core/ui/layouts/root-layout/root-layout-common.ts @@ -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 { @@ -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 @@ -77,47 +77,46 @@ export class RootLayoutBase extends GridLayout { close(view: View, exitTo?: TransitionAnimation): Promise { return new Promise((resolve, reject) => { if (this.hasChild(view)) { + const cleanupAndFinish = () => { + this.removeChild(view); + resolve(); + }; + 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]; + // use exitAnimation that is passed in and fallback to the exitAnimation passed in when opening + const exitAnimationDefinition = exitTo || poppedView?.options?.animation?.exitTo; + + // 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[] = [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); @@ -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 { + 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 { return new Promise((resolve, reject) => { @@ -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); } @@ -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 { @@ -330,21 +359,6 @@ export class RootLayoutBase extends GridLayout { return this.getChildIndex(view) >= 0; } - private closeShadeCover(shadeCoverOptions?: ShadeCoverOptions): Promise { - return new Promise((resolve) => { - // if shade cover is displayed and the last popup is closed, also close the shade cover - if (this.shadeCover) { - return this._closeShadeCover(this.shadeCover, shadeCoverOptions).then(() => { - this.removeChild(this.shadeCover); - this.shadeCover.off('loaded'); - this.shadeCover = null; - resolve(); - }); - } - resolve(); - }); - } - protected _bringToFront(view: View) {} protected _initShadeCover(view: View, shadeOption: ShadeCoverOptions): void {} @@ -356,6 +370,8 @@ export class RootLayoutBase extends GridLayout { protected _closeShadeCover(view: View, shadeOptions: ShadeCoverOptions): Promise { return new Promise(() => {}); } + + protected _cleanupPlatformShadeCover(): void {} } export function getRootLayout(): RootLayout { diff --git a/packages/core/ui/styling/background.ios.ts b/packages/core/ui/styling/background.ios.ts index 215c57102..e3eb08627 100644 --- a/packages/core/ui/styling/background.ios.ts +++ b/packages/core/ui/styling/background.ios.ts @@ -5,6 +5,7 @@ import { View, Point } from '../core/view'; import { LinearGradient } from './linear-gradient'; import { Color } from '../../color'; import { iOSNativeHelper, isDataURI, isFileOrResourcePath, layout } from '../../utils'; +import { ios as iosViewUtils, NativeScriptUIView } from '../utils'; import { ImageSource } from '../../image-source'; import { CSSValue, parse as cssParse } from '../../css-value'; import { CSSShadow } from './css-shadow'; @@ -13,23 +14,6 @@ import { BackgroundClearFlags } from './background-common'; export * from './background-common'; -interface NativeView extends UIView { - hasNonUniformBorder: boolean; - - borderLayer: CALayer; - - hasBorderMask: boolean; - borderOriginalMask: CALayer; - - topBorderLayer: CALayer; - rightBorderLayer: CALayer; - bottomBorderLayer: CALayer; - leftBorderLayer: CALayer; - - gradientLayer: CAGradientLayer; - boxShadowLayer: CALayer; -} - interface Rect { left: number; top: number; @@ -47,7 +31,7 @@ export enum CacheMode { export namespace ios { export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void { const background = view.style.backgroundInternal; - const nativeView = view.nativeViewProtected; + const nativeView = view.nativeViewProtected; if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) { // clear box shadow if it has been removed! @@ -60,9 +44,9 @@ export namespace ios { clearNonUniformBorders(nativeView); } - clearGradient(nativeView); + iosViewUtils.clearGradient(nativeView); if (background.image instanceof LinearGradient) { - drawGradient(nativeView, background.image); + iosViewUtils.drawGradient(nativeView, background.image); } const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder(); @@ -113,7 +97,7 @@ function onScroll(this: void, args: ScrollEventData): void { } } -function adjustLayersForScrollView(nativeView: UIScrollView & NativeView) { +function adjustLayersForScrollView(nativeView: UIScrollView & NativeScriptUIView) { const layer = nativeView.borderLayer; if (layer instanceof CALayer) { // Compensates with transition for the background layers for scrolling in ScrollView based controls. @@ -149,7 +133,7 @@ function subscribeForScrollNotifications(view: View) { } } -function clearNonUniformBorders(nativeView: NativeView): void { +function clearNonUniformBorders(nativeView: NativeScriptUIView): void { if (nativeView.borderLayer) { nativeView.borderLayer.removeFromSuperlayer(); } @@ -459,7 +443,7 @@ function cssValueToDeviceIndependentPixels(source: string, total: number): numbe } } -function drawUniformColorNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) { +function drawUniformColorNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) { const layer = nativeView.layer; layer.backgroundColor = undefined; layer.borderColor = undefined; @@ -587,7 +571,7 @@ function drawUniformColorNonUniformBorders(nativeView: NativeView, background: B nativeView.hasNonUniformBorder = true; } -function drawNoRadiusNonUniformBorders(nativeView: NativeView, background: BackgroundDefinition) { +function drawNoRadiusNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) { const borderLayer = CALayer.layer(); nativeView.layer.addSublayer(borderLayer); nativeView.borderLayer = borderLayer; @@ -726,7 +710,7 @@ function drawNoRadiusNonUniformBorders(nativeView: NativeView, background: Backg } // TODO: use sublayer if its applied to a layout -function drawBoxShadow(nativeView: NativeView, view: View, boxShadow: CSSShadow, background: BackgroundDefinition, useSubLayer: boolean = false) { +function drawBoxShadow(nativeView: NativeScriptUIView, view: View, boxShadow: CSSShadow, background: BackgroundDefinition, useSubLayer: boolean = false) { const layer: CALayer = iOSNativeHelper.getShadowLayer(nativeView, 'ns-box-shadow'); layer.masksToBounds = false; @@ -765,7 +749,7 @@ function drawBoxShadow(nativeView: NativeView, view: View, boxShadow: CSSShadow, layer.shadowPath = UIBezierPath.bezierPathWithRoundedRectCornerRadius(bounds, cornerRadius).CGPath; } -function clearBoxShadow(nativeView: NativeView) { +function clearBoxShadow(nativeView: NativeScriptUIView) { nativeView.clipsToBounds = true; const layer: CALayer = iOSNativeHelper.getShadowLayer(nativeView, 'ns-box-shadow', false); if (!layer) { @@ -779,46 +763,6 @@ function clearBoxShadow(nativeView: NativeView) { layer.shadowOpacity = 0.0; } -function drawGradient(nativeView: NativeView, gradient: LinearGradient) { - const gradientLayer = CAGradientLayer.layer(); - gradientLayer.frame = nativeView.bounds; - nativeView.gradientLayer = gradientLayer; - - const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length); - const iosStops = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length); - let hasStops = false; - - gradient.colorStops.forEach((stop) => { - iosColors.addObject(stop.color.ios.CGColor); - if (stop.offset) { - iosStops.addObject(stop.offset.value); - hasStops = true; - } - }); - - gradientLayer.colors = iosColors; - - if (hasStops) { - gradientLayer.locations = iosStops; - } - - const alpha = gradient.angle / (Math.PI * 2); - const startX = Math.pow(Math.sin(Math.PI * (alpha + 0.75)), 2); - const startY = Math.pow(Math.sin(Math.PI * (alpha + 0.5)), 2); - const endX = Math.pow(Math.sin(Math.PI * (alpha + 0.25)), 2); - const endY = Math.pow(Math.sin(Math.PI * alpha), 2); - gradientLayer.startPoint = { x: startX, y: startY }; - gradientLayer.endPoint = { x: endX, y: endY }; - - nativeView.layer.insertSublayerAtIndex(gradientLayer, 0); -} - -function clearGradient(nativeView: NativeView): void { - if (nativeView.gradientLayer) { - nativeView.gradientLayer.removeFromSuperlayer(); - } -} - function drawClipPath(nativeView: UIView, background: BackgroundDefinition) { const layer = nativeView.layer; const layerBounds = layer.bounds; diff --git a/packages/core/ui/styling/gradient.d.ts b/packages/core/ui/styling/gradient.d.ts deleted file mode 100644 index 170f58b22..000000000 --- a/packages/core/ui/styling/gradient.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { CoreTypes } from '../../core-types'; -import { Color } from '../../color'; -import { LinearGradient as LinearGradientDefinition } from '../../css/parser'; - -export class LinearGradient { - public angle: number; - public colorStops: ColorStop[]; - - public static parse(value: LinearGradientDefinition): LinearGradientDefinition; - - public static equals(first: LinearGradientDefinition, second: LinearGradientDefinition): boolean; -} - -export interface ColorStop { - color: Color; - offset?: CoreTypes.LengthPercentUnit; -} diff --git a/packages/core/ui/styling/linear-gradient.ts b/packages/core/ui/styling/linear-gradient.ts index 72b3cad6a..4d67834a8 100644 --- a/packages/core/ui/styling/linear-gradient.ts +++ b/packages/core/ui/styling/linear-gradient.ts @@ -1,8 +1,12 @@ import { CoreTypes } from '../../core-types'; import { Color } from '../../color'; -import { ColorStop } from './gradient'; import { LinearGradient as CSSLinearGradient } from '../../css/parser'; +export interface ColorStop { + color: Color; + offset?: CoreTypes.LengthPercentUnit; +} + export class LinearGradient { public angle: number; public colorStops: ColorStop[]; diff --git a/packages/core/ui/utils.d.ts b/packages/core/ui/utils.d.ts index f01d92c21..aff6f4070 100644 --- a/packages/core/ui/utils.d.ts +++ b/packages/core/ui/utils.d.ts @@ -1,4 +1,19 @@ -export namespace ios { +export interface NativeScriptUIView extends UIView { + hasNonUniformBorder: boolean; + borderLayer: CALayer; + + hasBorderMask: boolean; + borderOriginalMask: CALayer; + + topBorderLayer: CALayer; + rightBorderLayer: CALayer; + bottomBorderLayer: CALayer; + leftBorderLayer: CALayer; + + gradientLayer: CAGradientLayer; + boxShadowLayer: CALayer; +} +export namespace ios { /** * Gets actual height of a [UIView](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/) widget in device pixels. * @param uiView - An instance of UIView. @@ -10,4 +25,19 @@ * @param viewController when specified it is used to check preferStatusBarHidden property. */ export function getStatusBarHeight(viewController?: any): number; + + /** + * draw gradient using CAGradientLayer and insert into UIView sublayer + * @param nativeView UIView + * @param gradient Parsed LinearGradient + * @param gradientLayerOpacity Initial layer opacity (in case you'd like to use with animation sequence) + * @param index sublayer index to insert layer at (defaults to 0) + */ + export function drawGradient(uiView: any /* UIView */, gradient: LinearGradient, gradientLayerOpacity?: number, index?: number): any; /* CAGradientLayer */ + + /** + * clear gradientLayer if found on provided UIView + * @param nativeView UIView + */ + export function clearGradient(uiView: any /* UIView */): void; } diff --git a/packages/core/ui/utils.ios.ts b/packages/core/ui/utils.ios.ts index f34ef4260..fe6e370e9 100644 --- a/packages/core/ui/utils.ios.ts +++ b/packages/core/ui/utils.ios.ts @@ -1,4 +1,21 @@ import * as utils from '../utils'; +import { LinearGradient } from './styling/linear-gradient'; + +interface NativeScriptUIView extends UIView { + hasNonUniformBorder: boolean; + borderLayer: CALayer; + + hasBorderMask: boolean; + borderOriginalMask: CALayer; + + topBorderLayer: CALayer; + rightBorderLayer: CALayer; + bottomBorderLayer: CALayer; + leftBorderLayer: CALayer; + + gradientLayer: CAGradientLayer; + boxShadowLayer: CALayer; +} export namespace ios { export function getActualHeight(view: UIView): number { @@ -24,4 +41,51 @@ export namespace ios { return utils.layout.toDevicePixels(min); } + + export function drawGradient(nativeView: NativeScriptUIView, gradient: LinearGradient, gradientLayerOpacity?: number, index?: number): CAGradientLayer { + let gradientLayer: CAGradientLayer; + if (nativeView && gradient) { + gradientLayer = CAGradientLayer.layer(); + if (typeof gradientLayerOpacity === 'number') { + gradientLayer.opacity = gradientLayerOpacity; + } + gradientLayer.frame = nativeView.bounds; + nativeView.gradientLayer = gradientLayer; + + const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length); + const iosStops = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length); + let hasStops = false; + + gradient.colorStops.forEach((stop) => { + iosColors.addObject(stop.color.ios.CGColor); + if (stop.offset) { + iosStops.addObject(stop.offset.value); + hasStops = true; + } + }); + + gradientLayer.colors = iosColors; + + if (hasStops) { + gradientLayer.locations = iosStops; + } + + const alpha = gradient.angle / (Math.PI * 2); + const startX = Math.pow(Math.sin(Math.PI * (alpha + 0.75)), 2); + const startY = Math.pow(Math.sin(Math.PI * (alpha + 0.5)), 2); + const endX = Math.pow(Math.sin(Math.PI * (alpha + 0.25)), 2); + const endY = Math.pow(Math.sin(Math.PI * alpha), 2); + gradientLayer.startPoint = { x: startX, y: startY }; + gradientLayer.endPoint = { x: endX, y: endY }; + + nativeView.layer.insertSublayerAtIndex(gradientLayer, index || 0); + } + return gradientLayer; + } + + export function clearGradient(nativeView: NativeScriptUIView): void { + if (nativeView?.gradientLayer) { + nativeView.gradientLayer.removeFromSuperlayer(); + } + } }