feat(root-layout): support gradient colors on shade cover (#9626)

Co-authored-by: William Juan <williamjuan027@gmail.com>
This commit is contained in:
Nathan Walker
2022-01-13 21:41:52 -08:00
committed by GitHub
parent 2337d29a05
commit 06c00d2252
11 changed files with 252 additions and 167 deletions

View File

@ -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,
},

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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<void> {
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<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);
@ -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) => {
@ -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<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 {}
@ -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 {

View File

@ -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 = <NativeView>view.nativeViewProtected;
const nativeView = <NativeScriptUIView>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<number>().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;

View File

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

View File

@ -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[];

View File

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

View File

@ -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<number>().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();
}
}
}